Motion for React 实战模式手册:从入场动画到滚动驱动的 9 种典型用法
前言
在一个追求「克制但精致」动效体验的博客项目中,Motion(原 Framer Motion)几乎是绑定在每个交互细节上的基础设施——从首页标题的逐字模糊入场,到导航栏 hover 时的共享高亮滑块,再到阅读进度环的弹簧物理,30 多个文件里散落着各种用法。
本文把这些实战场景收拢为 9 种可复用的动效模式,每种模式都从「为什么需要」出发,给出最小可运行的代码骨架,然后展示项目中的真实实现。如果你正在用 Motion 但总觉得只会 initial + animate,希望这篇手册能帮你打开更多可能性。
关于命名:项目依赖的包名是
motion,导入路径统一为motion/react。文中的「Motion」即指这一生态。
一、入场动画:initial + animate + transition
这是 Motion 最基础也最高频的模式。核心思路:声明元素的「初始状态」和「目标状态」,Motion 自动补间。
1.1 最小示例
1.2 项目实战:文章详情页的分段入场
在博客的文章详情页里,标题、元信息、正文、作者卡片、评论区分别设置不同的 delay,形成由上至下的「瀑布式」入场节奏:
要点:
type: "spring"让运动更自然,不需要手动指定duration(但也可以同时指定)- 自定义贝塞尔曲线
ease: [0.22, 1, 0.36, 1]可以实现先快后慢的「出场」手感 - 多区块之间通过递增的
delay编排节奏,比一股脑全部淡入更有层次
二、视口触发:useInView 与 whileInView
很多动画不应在页面加载时就触发,而是当用户滚动到可见区域时才开始。Motion 提供了两种方式。
2.1 useInView Hook(命令式)
适合需要精确控制时机的场景——比如进入视口后再延迟一段时间才触发。
这段代码来自项目中的签名描边动画组件:元素进入视口 50% 后(amount: 0.5),再等一个自定义延迟才开始 SVG 路径绘制。once: true 确保动画只播放一次。
2.2 whileInView(声明式)
更简洁,适合「进视口就动」的场景:
选择建议:
三、编排多元素:variants + staggerChildren
当一组元素需要「依次出场」时,variants 比手动写 N 个 delay 优雅得多。
3.1 核心概念
父容器声明 staggerChildren,子元素只需挂 variants 即可自动获得错开的延迟。
3.2 项目实战:文章元信息 + 标签行
技巧:子元素的 transition.delay 可以叠加一个固定偏移量(如 1.4),让整组动画在页面入场动画的后半段才开始,形成更自然的衔接。
四、退出过渡:AnimatePresence
React 的条件渲染({show && <div>...</div>})在卸载时是瞬间消失的。AnimatePresence 允许子组件在被移除前播放退出动画。
4.1 图标切换:mode="popLayout"
项目中的「双状态切换按钮」用于 主题切换、菜单开关 等场景:
关键细节:
mode="popLayout":退出元素在退出动画期间会脱离文档流(position: absolute),新元素可以立即占位,避免「两个元素同时占空间」的跳动mode="wait":新元素等退出动画完成后才入场,适合内容切换initial={false}:组件首次挂载时不播放入场动画
4.2 移动端导航展开/收起
注意 height: "auto" ——Motion 可以在确定值和 auto 之间做动画,这在原生 CSS 中是非常难实现的。
五、共享布局动画:layoutId 与 LayoutGroup
layoutId 是 Motion 最「魔法」的能力之一:两个不同的 motion.* 元素只要共享同一个 layoutId,Motion 就会自动在它们之间生成平滑的位置 + 尺寸过渡。
5.1 导航栏 Hover 指示器
项目中的顶部导航栏,鼠标在菜单项之间移动时,高亮背景会平滑地「滑过去」:
原理:同一时刻只有一个 hoveredItem 匹配,所以只有一个 motion.span 被渲染。但由于它们共享 layoutId="nav-hover",Motion 会将前一个元素的位置「过渡到」新元素的位置,视觉上就像一个指示器在滑动。
5.2 LayoutGroup 的作用
当多个独立组件实例各自使用 layoutId 时,可能会产生命名冲突(比如两个列表都有 layoutId="hover")。用 LayoutGroup 包裹可以创建隔离的命名空间:
六、滚动驱动动画:useScroll + useTransform
这是 Motion 最强大的模式之一——将滚动位置直接映射为动画属性,实现「滚动即动画」。
6.1 底部高光线延展效果
项目中页面底部有一条柔金色高光线,随着 footer 进入视口从中间向两侧延展:
数据流图解:
要点:
useScroll({ target, offset })可以追踪特定元素相对于视口的滚动进度offset: ["start end", "start 0.7"]表示「元素顶部碰到视口底部」时 progress=0,「元素顶部到达视口 70% 处」时 progress=1- 中间插入
useSpring可以让进度变化更平滑,避免生硬的线性跟随
6.2 阅读进度环
文章详情页的「灵动岛目录」组件中,用 SVG 圆环显示阅读进度:
将页面整体滚动进度映射为 SVG 的 strokeDashoffset,进度从 0 到 100% 时,圆环从空到满。弹簧参数 damping: 20 比高光线的 50 更低,所以进度环会有轻微的「弹性回弹」感,更加灵动。
七、弹簧物理:useSpring 与 useMotionValue
useSpring 不仅可以搭配 useScroll,还可以单独驱动任何数值动画。
7.1 打字机宽度弹簧
项目中的 Typewriter 组件需要在文字增删时平滑调整容器宽度:
核心思路:
- 用一个隐藏的
<span ref={measureRef}>测量当前文字的真实宽度 - 通过
smoothWidth.set(width)更新弹簧的目标值 motion.span的style={{ width: smoothWidth }}自动跟随弹簧值平滑过渡
这比用 CSS transition: width 好在哪里?弹簧物理会产生自然的「过冲-回弹」效果,而不是匀速或贝塞尔缓动。
7.2 useMotionValue 的适用场景
useMotionValue 创建一个不触发 React 重渲染的响应式值,适合高频更新的场景:
如果用 useState 替代,每帧都会触发 React 重渲染,性能会急剧下降。useMotionValue + style 绑定走的是 Motion 内部的直接 DOM 更新通道,完全绕过了 React 调和。
八、SVG 路径描边动画:pathLength
Motion 对 SVG 有一等支持,motion.path 可以直接动画化 pathLength 属性,实现手写签名、图标描边等效果。
项目实战:签名动画
实现细节:
- 签名被拆分为多个笔画(
strokes),每笔独立配置duration和delay - 通过预计算每笔的「绝对开始时间」(
absoluteDelay),精确编排多笔画的先后顺序 pathLength: 0 → 1控制描边从无到有,opacity几乎瞬间切换(duration: 0.01),避免笔画在描绘前可见
九、逐帧动画:useAnimationFrame + 滚动速度
当需要每帧精确控制动画时(如无限滚动跑马灯),useAnimationFrame 是最底层的钩子。
项目实战:滚动速度驱动的跑马灯
这段代码构建了一个「滚动速度 → 弹簧平滑 → 速度因子」的管线。然后在 useAnimationFrame 中每帧更新位移:
性能守则:
- 检查
isInViewRef:元素不可见时跳过计算 - 检查
isPageVisibleRef:页面隐藏时暂停(visibilitychange) - 检查
prefers-reduced-motion:尊重系统无障碍设置 - 使用
useMotionValue而非useState:避免每帧触发 React 重渲染 - 添加
will-change-transform+transform-gpu:提示浏览器启用 GPU 合成
模式速查表
写在最后
Motion 的 API 设计哲学是声明式优先、逐步下沉:
- 大多数场景用
initial+animate+variants就能覆盖 - 需要滚动联动时引入
useScroll+useTransform - 需要极致性能时用
useMotionValue+useAnimationFrame绕过 React 渲染
克制是最好的动效策略。项目中遵循的时长参考:
- 快速交互(hover / tap):
0.15s - 0.3s - 常规过渡(入场 / 切换):
0.4s - 0.6s - 入场动画(页面级):
0.6s - 1s - 环境动画(呼吸 / 光晕):
8s - 10s
动效的目的是引导注意力和传达状态变化,而不是「秀技术」。当用户注意到动画本身时,往往意味着它太过了。好的动效应该像呼吸一样自然——存在,但不打扰。