framer-motion
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFramer Motion — Sub-skill
Framer Motion — 子技能
Package:(v11+, formerlymotion). Import:framer-motionimport { motion, AnimatePresence } from "motion/react"
包:(v11+,原motion)。导入方式:framer-motionimport { motion, AnimatePresence } from "motion/react"
When to use Framer Motion vs alternatives
何时选择 Framer Motion 而非其他方案
| Criteria | Framer Motion | GSAP | Native CSS |
|---|---|---|---|
| Layout animations | Excellent (layoutId) | Manual | Impossible |
| Exit animations | AnimatePresence | Timeline reverse | Limited (display) |
| Gestures (drag, hover) | Native, declarative | Draggable plugin | Basic |
| Scroll-driven | useScroll + useTransform | ScrollTrigger (more powerful) | scroll-timeline |
| Complex orchestration | Variants + propagation | Timeline (more flexible) | @keyframes |
| Bundle size | ~50kb tree-shaken | ~30kb core | 0kb |
| React integration | Native, component-first | Refs + useGSAP | className toggle |
Rule: Framer Motion for React UI interactions (modals, toasts, reorder, shared layout). GSAP for complex timelines, cinematic scroll-driven, SVG morphing.
| 评估标准 | Framer Motion | GSAP | 原生CSS |
|---|---|---|---|
| 布局动画 | 优秀(支持layoutId) | 需手动实现 | 无法实现 |
| 退出动画 | 支持AnimatePresence | 需通过时间线反转实现 | 受限(依赖display属性) |
| 手势(拖拽、悬停) | 原生支持,声明式写法 | 需使用Draggable插件 | 仅基础支持 |
| 滚动驱动动画 | useScroll + useTransform | ScrollTrigger(功能更强大) | scroll-timeline |
| 复杂动画编排 | Variants + 传播机制 | Timeline(灵活性更高) | @keyframes |
| 包体积 | 约50kb(摇树优化后) | 约30kb(核心包) | 0kb |
| React集成 | 原生支持,组件优先 | Refs + useGSAP | 切换className实现 |
原则:React UI交互(模态框、提示框、重排序、共享布局)使用Framer Motion。复杂时间线、电影级滚动驱动动画、SVG变形使用GSAP。
AnimatePresence — Exit animations
AnimatePresence — 退出动画
tsx
<AnimatePresence mode="wait">
{isVisible && (
<motion.div
key="unique-key" // REQUIRED — identifies the component
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>- — waits for exit to finish before enter (page transitions)
mode="wait" - — exit and enter simultaneously
mode="sync" - — removes from flow immediately (good for lists)
mode="popLayout" - — callback when all exit animations are finished
onExitComplete
tsx
<AnimatePresence mode="wait">
{isVisible && (
<motion.div
key="unique-key" // 必填 — 标识组件
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>- — 等待退出动画完成后再执行进入动画(适用于页面过渡)
mode="wait" - — 退出和进入动画同时执行
mode="sync" - — 立即从文档流中移除(适用于列表场景)
mode="popLayout" - — 所有退出动画完成后的回调函数
onExitComplete
Layout animations
布局动画
tsx
// Shared layout — the element "slides" between two positions
<motion.div layoutId="highlight" className={activeTab === id ? "active" : ""} />
// Auto layout — animates position/size when layout changes
<motion.div layout>
{isExpanded && <motion.p layout>Additional content</motion.p>}
</motion.div>
// layout="position" — animates position only (not size)
// layout="size" — animates size only
// layout="preserve-aspect" — preserves the ratio during the transitiontsx
// 共享布局 — 元素在两个位置间“滑动”过渡
<motion.div layoutId="highlight" className={activeTab === id ? "active" : ""} />
// 自动布局 — 布局变化时自动动画位置/尺寸
<motion.div layout>
{isExpanded && <motion.p layout>额外内容</motion.p>}
</motion.div>
// layout="position" — 仅动画位置(不包含尺寸)
// layout="size" — 仅动画尺寸(不包含位置)
// layout="preserve-aspect" — 过渡期间保持宽高比Variants — Propagation and orchestration
Variants — 传播与编排
tsx
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.08,
delayChildren: 0.2,
staggerDirection: 1, // 1 = normal, -1 = reverse
},
},
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 },
};
<motion.ul variants={container} initial="hidden" animate="show">
{items.map((i) => (
<motion.li key={i.id} variants={item} />
))}
</motion.ul>Variants automatically propagate to motion children — no need for / on children.
initialanimatetsx
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.08,
delayChildren: 0.2,
staggerDirection: 1, // 1 = 正序, -1 = 倒序
},
},
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 },
};
<motion.ul variants={container} initial="hidden" animate="show">
{items.map((i) => (
<motion.li key={i.id} variants={item} />
))}
</motion.ul>Variants会自动传播给子motion组件 — 无需在子组件上设置/。
initialanimateGestures
手势
tsx
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
whileFocus={{ borderColor: "#3b82f6" }}
// Drag
drag // true = x+y, "x" = horizontal only, "y" = vertical only
dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
dragElastic={0.2} // 0 = rigid, 1 = free (default 0.35)
dragSnapToOrigin // returns to initial position
onDragEnd={(e, info) => {
if (info.offset.x > 100) handleSwipe("right");
}}
/>tsx
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
whileFocus={{ borderColor: "#3b82f6" }}
// 拖拽
drag // true = 允许x+y方向拖拽, "x" = 仅水平方向, "y" = 仅垂直方向
dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
dragElastic={0.2} // 0 = 完全刚性, 1 = 完全自由(默认值0.35)
dragSnapToOrigin // 拖拽结束后返回初始位置
onDragEnd={(e, info) => {
if (info.offset.x > 100) handleSwipe("right");
}}
/>Motion values — Reactive without re-render
Motion值 — 无重渲染的响应式更新
tsx
const x = useMotionValue(0);
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0]);
const background = useTransform(x, [-200, 200], ["#ff0000", "#00ff00"]);
// Spring-based smoothing
const smoothX = useSpring(x, { stiffness: 300, damping: 30 });
// Scroll tracking
const { scrollY, scrollYProgress } = useScroll();
const parallaxY = useTransform(scrollYProgress, [0, 1], [0, -300]);
// Element-scoped scroll
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"],
});Motion values do NOT trigger React re-renders — they update the DOM directly via .
styletsx
const x = useMotionValue(0);
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0]);
const background = useTransform(x, [-200, 200], ["#ff0000", "#00ff00"]);
// 基于弹簧的平滑处理
const smoothX = useSpring(x, { stiffness: 300, damping: 30 });
// 滚动追踪
const { scrollY, scrollYProgress } = useScroll();
const parallaxY = useTransform(scrollYProgress, [0, 1], [0, -300]);
// 元素作用域内的滚动
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"],
});Motion值不会触发React重渲染 — 它们通过直接更新DOM。
styleDo Not
注意事项
Do not setState in callbacks without a guard
不要在回调中无保护地调用setState
tsx
// BAD — infinite re-render if animate depends on state
onUpdate={(latest) => setPosition(latest.x)}
// GOOD — guard or useMotionValueEvent
const x = useMotionValue(0);
useMotionValueEvent(x, "change", (latest) => {
if (latest > threshold) onThresholdReached();
});tsx
// 错误 — 如果动画依赖状态,会导致无限重渲染
onUpdate={(latest) => setPosition(latest.x)}
// 正确 — 添加保护或使用useMotionValueEvent
const x = useMotionValue(0);
useMotionValueEvent(x, "change", (latest) => {
if (latest > threshold) onThresholdReached();
});Do not use layout animation without a stable key
不要在没有稳定key的情况下使用布局动画
tsx
// BAD — key changes every render, breaks layout tracking
<motion.div layout key={Math.random()} />
// GOOD — stable key derived from data
<motion.div layout key={item.id} />tsx
// 错误 — key每次渲染都变化,破坏布局追踪
<motion.div layout key={Math.random()} />
// 正确 — 使用数据派生的稳定key
<motion.div layout key={item.id} />Do not forget the unique key on AnimatePresence
不要忘记给AnimatePresence的子元素设置唯一key
tsx
// BAD — no key, exit animation does not trigger
<AnimatePresence>
{isOpen && <motion.div exit={{ opacity: 0 }} />}
</AnimatePresence>
// GOOD — unique key for each conditional child
<AnimatePresence>
{isOpen && <motion.div key="modal" exit={{ opacity: 0 }} />}
</AnimatePresence>tsx
// 错误 — 没有key,退出动画不会触发
<AnimatePresence>
{isOpen && <motion.div exit={{ opacity: 0 }} />}
</AnimatePresence>
// 正确 — 为每个条件渲染的子元素设置唯一key
<AnimatePresence>
{isOpen && <motion.div key="modal" exit={{ opacity: 0 }} />}
</AnimatePresence>Do not wrap an already animated component with motion.div
不要用motion.div包裹已经有动画的组件
tsx
// BAD — double animation, transform conflicts
<motion.div animate={{ x: 100 }}>
<motion.div animate={{ x: -50 }}>Content</motion.div>
</motion.div>
// GOOD — single animation level per transform axis
<motion.div animate={{ x: 100 }}>
<motion.div animate={{ opacity: 0.5 }}>Content</motion.div>
</motion.div>
// GOOD — use variants to coordinate parent/child
<motion.div variants={parent} animate="active">
<motion.div variants={child} />
</motion.div>tsx
// 错误 — 双重动画,transform属性冲突
<motion.div animate={{ x: 100 }}>
<motion.div animate={{ x: -50 }}>内容</motion.div>
</motion.div>
// 正确 — 每个变换轴仅设置一层动画
<motion.div animate={{ x: 100 }}>
<motion.div animate={{ opacity: 0.5 }}>内容</motion.div>
</motion.div>
// 正确 — 使用variants协调父/子组件动画
<motion.div variants={parent} animate="active">
<motion.div variants={child} />
</motion.div>