关于 react-animations
面向 React 开发者的动画技能,指导选择并使用 Framer Motion、React Spring、GSAP 或 CSS/Tailwind。内容涵盖实现、无障碍与性能,并提供可复用组件和变体模板。
主要功能
- 01动画库选择建议
- 02Framer Motion 手势与布局模式
- 03滚动、拖拽和时间轴动画指南
- 04减少动态效果的无障碍实践
- 05性能与包体积建议
- 06可复用动画组件和变体
适用场景
- 01为 React 界面选择动画方案
- 02添加滚动触发或共享元素过渡
- 03检查动画的无障碍性和性能
- 04构建可复用的游戏 UI 动画
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ fcsouza/ agent-skills --skill react-animations安装后在 Claude Code 中输入/react-animations确认已加载。
附带文件boilerplate/motion-components.tsxtemplates/animation-variants.ts
详细介绍
React 动画
使用 Framer Motion(主要)、React Spring(物理效果)、GSAP(时间轴)和 CSS/Tailwind(简单场景),在 React 中实现适用于生产环境的高质量动画。
库选择表
| 使用场景 | 库 | 原因 |
|---|---|---|
| 入场/退场动画 | Framer Motion | AnimatePresence 可处理卸载 |
| 共享元素过渡 | Framer Motion | layoutId |
| 基于物理效果(弹簧、反弹) | Framer Motion 或 React Spring | 弹簧配置 |
| 复杂时间轴/序列 | GSAP | Timeline API |
| 滚动触发 | Framer Motion(whileInView / useScroll) | 内置滚动钩子 |
| 简单的悬停/聚焦状态 | CSS Tailwind | 无需 JS |
| 拖放 | Framer Motion | 内置手势支持 |
| SVG 路径动画 | GSAP 或 Framer Motion | 两者都支持 SVG |
| 命令式/程序化动画 | Framer Motion useAnimate | 现代命令式 API |
核心原则
Matt Perry(Framer Motion 创作者):“动画应该以声明式方式定义,而不是以命令式方式管理。描述目标状态,其余工作交给库处理。” Sarah Drasner:“动画不是装饰,而是沟通。每个动态效果都应该有明确目的。”
- 简单情况用 CSS,复杂情况用 JS — 如果 Tailwind
transition能满足需求,就用它;不要为了悬停状态添加 Framer Motion - 只对合成属性应用动画 — 使用
transform和opacity;不要使用width、height、top、left(会导致重排) - 用 AnimatePresence 包裹条件渲染 — 否则会跳过退场动画
- 用变体协调动画 — 在组件外部将动画状态定义为对象,不要使用内联值
- 用 layoutId 实现共享元素过渡 — Framer Motion 会处理位置之间的插值
- 手势驱动时使用 useMotionValue — 不要对驱动动画的值使用 useState
- 将帧率预算保持在 60fps — 不要在渲染周期中执行动画逻辑;使用变换
Framer Motion 核心模式
基本动画
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
/>
手势状态(whileHover、whileTap)
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
/>
带子元素交错效果的变体
const container = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.1 },
},
};
const item = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};
<motion.ul variants={container} initial="hidden" animate="visible">
{items.map((i) => (
<motion.li key={i} variants={item} />
))}
</motion.ul>
使用 AnimatePresence 实现退场动画
mode 属性控制进入和退出的元素如何交互:
"sync"(默认)— 进入和退出同时发生"wait"— 退出完成后才开始进入(适用于页面过渡)"popLayout"— 立即将退出元素从布局流中移除
<AnimatePresence mode="wait">
{isVisible && (
<motion.div
key="modal"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
/>
)}
</AnimatePresence>
使用 whileInView 实现滚动触发动画
最简单的方法,无需滚动钩子:
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-100px' }}
transition={{ duration: 0.5 }}
/>
设置 viewport.once: true,这样滚动回去时动画就不会重播。
使用 useScroll + useTransform 实现视差效果
const { scrollYProgress } = useScroll();
const y = useTransform(scrollYProgress, [0, 1], [0, -200]);
<motion.div style={{ y }} />
使用 layoutId 实现共享元素
// In list view
<motion.div layoutId={`card-${id}`}>
<Thumbnail />
</motion.div>
// In detail view
<motion.div layoutId={`card-${id}`}>
<FullImage />
</motion.div>
带约束的拖动
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
dragElastic={0.2}
whileDrag={{ scale: 1.1 }}
/>
useAnimate — 命令式动画(v11+)
对于程序化序列,优先使用 useAnimate,而不是 useAnimation。它的作用范围限定在 ref 内,并且可在该范围内与任意选择器配合使用:
const [scope, animate] = useAnimate();
const handleClick = async () => {
await animate(scope.current, { scale: 1.2 }, { duration: 0.2 });
await animate(scope.current, { scale: 1 }, { duration: 0.1 });
};
<div ref={scope}>
<button onClick={handleClick}>Click me</button>
</div>
MotionConfig — 全局动画设置
包裹整个应用(或一个子树),即可设置减少动态效果或弹簧预设等默认选项:
<MotionConfig reducedMotion="user">
<App />
</MotionConfig>
reducedMotion="user" 会自动为在操作系统级别设置了“减少动态效果”偏好的用户禁用动画。这是大规模实现无障碍支持最简单的方式。
LazyMotion — 优化包体积
对于生产环境中的应用,可将 motion 替换为 LazyMotion + m,以便对动画引擎进行代码分割:
import { LazyMotion, domAnimation, m } from 'framer-motion';
<LazyMotion features={domAnimation}>
<m.div animate={{ opacity: 1 }} />
</LazyMotion>
如果需要拖动或布局动画,请使用 domMax,而不是 domAnimation。
游戏 UI 模式
平滑缓动生命值条
const motionWidth = useMotionValue(current / max);
const springWidth = useSpring(motionWidth, { stiffness: 200, damping: 30 });
useEffect(() => {
motionWidth.set(Math.max(0, Math.min(1, current / max)));
}, [current, max]);
<motion.div style={{ scaleX: springWidth, transformOrigin: 'left' }} />
向上浮动的伤害数字
<motion.span
initial={{ opacity: 1, y: 0 }}
animate={{ opacity: 0, y: -60 }}
transition={{ duration: 0.8, ease: 'easeOut' }}
onAnimationComplete={onComplete}
>
-{damage}
</motion.span>
卡片翻转(rotateY)
<motion.div animate={{ rotateY: isFlipped ? 180 : 0 }} style={{ perspective: 1000 }}>
<div style={{ backfaceVisibility: 'hidden' }}>{front}</div>
<div style={{ backfaceVisibility: 'hidden', rotateY: 180 }}>{back}</div>
</motion.div>
屏幕抖动(useAnimate)
const [scope, animate] = useAnimate();
const shake = async () => {
await animate(scope.current, { x: [0, -10, 10, -10, 10, 0] }, { duration: 0.4 });
};
<div ref={scope}>{children}</div>
菜单滑入/滑出
<AnimatePresence>
{isOpen && (
<motion.nav
initial={{ x: -300 }}
animate={{ x: 0 }}
exit={{ x: -300 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
/>
)}
</AnimatePresence>
物品栏物品掉落(弹簧物理效果)
<motion.div
initial={{ y: -200, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ type: 'spring', stiffness: 400, damping: 15 }}
/>
无障碍
始终尊重用户对动态效果的偏好。以下是两种实现方式:
1. MotionConfig(推荐用于应用) — 包裹整个组件树:
<MotionConfig reducedMotion="user">
<App />
</MotionConfig>
2. useReducedMotion 钩子 — 用于组件级控制:
const shouldReduceMotion = useReducedMotion();
<motion.div
animate={{ opacity: 1, y: shouldReduceMotion ? 0 : -20 }}
/>
性能
- 仅对
transform和opacity进行动画处理——由 GPU 合成,不会触发布局或绘制 - 对始终执行动画的元素使用
will-change: transform(提升到独立图层) - 使用
motion.create(Component)为自定义组件添加动画,无需包装<div> layout属性会触发自动布局动画——在大型 DOM 树中开销较高;应将范围限定在尽可能小的子树中- 对驱动动画的值,优先使用
useMotionValue而不是useState——运动值不会触发重新渲染 - 使用
LazyMotion和domAnimation,与完整包相比可减少约 15kb gzip 体积
设置
bun add framer-motion
无需配置——不需要任何 Provider。直接导入并使用 motion.div。
如需进行全局无障碍处理,请包装应用根节点:
import { MotionConfig } from 'framer-motion';
<MotionConfig reducedMotion="user">
<App />
</MotionConfig>
样板代码
boilerplate/motion-components.tsx — 可直接使用的组件:FadeIn、SlideIn、ScaleIn、StaggerList、FloatingNumber、HealthBar、AnimatedCard
templates/animation-variants.ts — 可复用的变体对象和弹簧过渡预设
交叉引用
vercel-react-best-practices— React 性能模式ui-ux-game— 游戏 HUD 和 UI 模式frontend-design— 组件设计和 Tailwind 模式
常见问题与反模式
- 为会触发布局的属性添加动画(
width、height、top、left)——改用scaleX/scaleY或layout属性 - 使用
exit属性时忘记 AnimatePresence——没有包装器时,退出动画会被静默跳过 - 在渲染期间创建运动值——使用
useMotionValueHook;在渲染期间创建会导致内存泄漏 - 在同一元素上同时使用 CSS
transition和 Framer Motion——二者会冲突;选择其中一种 - 过度使用动画——每次交互都添加动画会造成感官过载;动画应传达信息,而非仅作装饰
- 挂载时添加动画却未设置
initial——组件会在动画开始前闪现;务必设置initial - 使用
useAnimation——已弃用;需要命令式动画时改用useAnimate - 大型
layout动画——在深层嵌套树中使用layout属性会导致昂贵的重新计算;应将范围限定在尽可能小的子树中 - 忽略无障碍功能——务必使用
MotionConfig reducedMotion="user"或useReducedMotion
来源
- Framer Motion 文档 — https://motion.dev
- Matt Perry — Framer Motion 创作者,API 设计演讲
- Sarah Drasner —《SVG 动画》,动画设计模式
- GSAP 文档 — https://gsap.com