这是一款用 React 19 + Motion + Tailwind CSS 实现的图片拖尾特效:页面中央写着大字标题,鼠标在页面上滑动时,每移动一段距离就会在光标位置生成一张倾斜的照片,先按弹簧曲线弹出、维持 1 秒后再缩小淡出。四张图循环使用,倾斜角度随机在 ±20°,视觉上像一串抛出的照片。核心逻辑不到 130 行,节流靠「时间间隔 + 距离阈值」双门槛,避免密集鼠标事件把 DOM 撑爆,是任何创意 Landing Page 都能塞进去的一段亮眼交互。
这款图片拖尾特效的四个亮点
节流做了双限制:`now - lastTime.current < DELAY_MS`(50ms 节流)和 `distance < THRESHOLD`(80px 位移阈值)任一满足就跳过,鼠标快慢都不会刷屏。
每张图带唯一 id(`时间戳-随机 9 位`),Motion 用 id 作 key 稳定跟踪;`setTimeout` 在 1 秒后 filter 掉自己,`AnimatePresence` 自然触发退场动画。
入场用 `type: "spring", stiffness: 260, damping: 20`,是 Motion 里比较有「果冻感」的默认;退场单独用 `{ duration: 0.4, ease: "easeIn" }`,快速收敛不拖尾。
图片走 `position: fixed` 加 `-translate-x-1/2 -translate-y-1/2` 精准以光标为中心;容器加 `pointer-events-none z-50`,覆盖在所有内容之上但不影响下方点击。
Motion + 生命周期管理的三个门道
其一,`AnimatePresence` 必须放在数组 map 的外层,且每个子元素必须有稳定的 key,否则退场动画不会触发——常见的坑是用 index 当 key,元素被复用退场就会静默失败。其二,setTimeout 与 React 状态更新有一个小时序陷阱:如果组件卸载但 setTimeout 未清理,会警告 setState on unmounted;本模板用 `setTrail(prev => prev.filter(...))` 的函数式写法回避了闭包过期问题。其三,图片列表放在 `useRef` 而非 state(`imageIndex.current`),避免每次触发都重新渲染整个 App。源码中均有注释可查看完整实现。
要换图片或调节奏怎么改
图片列表在 `IMAGE_SOURCES` 数组(约第 5 行),换成自己的图片路径,四张五张十张都行;建议图片尺寸控制在 300px 以内以减少下载。要让拖尾更密集,把 `THRESHOLD` 从 80 调低到 40、`DELAY_MS` 从 50 调低到 30,会有明显更浓的拖尾;反过来想稀疏,两个值都调大。要延长每张停留时间,改 `DURATION_MS`。要在移动端也触发,把 `mousemove` 换成 `pointermove` 并处理触摸事件的 targetTouches。
浏览器兼容性:需支持 ES Modules;React 19 与 Motion 从 esm.sh CDN 加载,生产环境建议本地打包。移动端触摸场景需要改事件绑定。详细运行方法请参见压缩包内的帮助文档。
常见问题
拖尾图片会不会内存泄漏
不会。每张图 1 秒后由 setTimeout 从 state 里 filter 掉,元素立即卸载;如果担心 setTimeout 累积,可以在 useEffect 的清理函数里额外收敛。
能改成鼠标点击才出现一张吗
能。把 `mousemove` 监听改成 `click`,去掉节流判断即可,每次点击都掉一张图,配合略长的 `DURATION_MS` 视觉上会更像点赞飞星。
怎么让图片按顺序而不是循环出现
把 `imageIndex.current = (imageIndex.current + 1) % IMAGE_SOURCES.length` 里的取模去掉,配合边界判断到最后一张就停止,即可做出「一次性剧本」的效果。
移动端触摸能触发吗
默认不能,因为只监听了 mousemove。把事件换成 `pointermove` 并 fallback 到 `touchmove`,同时给 body 加 `touch-action: none` 防止滑动被浏览器接管即可。
适合放在哪些页面
适合摄影/设计师作品集主页、创意机构官网首屏、婚礼/活动纪念页面、艺术展 Landing Page、任何需要「鼠标滑过留下惊喜」的交互场景。下载源码后可作为独立模块嵌入任何 React 项目。