framer-motion

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Framer Motion — Sub-skill

Framer Motion — 子技能

Package:
motion
(v11+, formerly
framer-motion
). Import:
import { motion, AnimatePresence } from "motion/react"
包:
motion
(v11+,原
framer-motion
)。导入方式:
import { motion, AnimatePresence } from "motion/react"

When to use Framer Motion vs alternatives

何时选择 Framer Motion 而非其他方案

CriteriaFramer MotionGSAPNative CSS
Layout animationsExcellent (layoutId)ManualImpossible
Exit animationsAnimatePresenceTimeline reverseLimited (display)
Gestures (drag, hover)Native, declarativeDraggable pluginBasic
Scroll-drivenuseScroll + useTransformScrollTrigger (more powerful)scroll-timeline
Complex orchestrationVariants + propagationTimeline (more flexible)@keyframes
Bundle size~50kb tree-shaken~30kb core0kb
React integrationNative, component-firstRefs + useGSAPclassName toggle
Rule: Framer Motion for React UI interactions (modals, toasts, reorder, shared layout). GSAP for complex timelines, cinematic scroll-driven, SVG morphing.
评估标准Framer MotionGSAP原生CSS
布局动画优秀(支持layoutId)需手动实现无法实现
退出动画支持AnimatePresence需通过时间线反转实现受限(依赖display属性)
手势(拖拽、悬停)原生支持,声明式写法需使用Draggable插件仅基础支持
滚动驱动动画useScroll + useTransformScrollTrigger(功能更强大)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>
  • mode="wait"
    — waits for exit to finish before enter (page transitions)
  • mode="sync"
    — exit and enter simultaneously
  • mode="popLayout"
    — removes from flow immediately (good for lists)
  • onExitComplete
    — callback when all exit animations are finished
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 transition
tsx
// 共享布局 — 元素在两个位置间“滑动”过渡
<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
initial
/
animate
on children.
tsx
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组件 — 无需在子组件上设置
initial
/
animate

Gestures

手势

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
style
.
tsx
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重渲染 — 它们通过
style
直接更新DOM。

Do 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>