framer-motion

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Motion (Framer Motion) — Animation Skill

Motion(原Framer Motion)—— 动画技能

Production-grade animation patterns for React and Next.js. This skill helps you write correct, performant, accessible animations using the Motion library (v12+).
适用于React和Next.js的生产级动画模式。本技能可帮助你使用Motion库(v12+)编写正确、高性能且易用的动画。

Imports

导入方式

Motion rebranded from
framer-motion
to
motion
. Both package names work, but the import path matters:
tsx
// Client Components (standard React)
import { motion, AnimatePresence } from "motion/react"

// Next.js Server Components — use the client export
import * as motion from "motion/react-client"

// Legacy import (still works with the framer-motion package)
import { motion } from "framer-motion"
If the project already uses
framer-motion
as a dependency, keep using
"framer-motion"
imports for consistency. Don't mix import sources.
Motion已从
framer-motion
更名为
motion
。两个包名均有效,但导入路径有讲究:
tsx
// Client Components (standard React)
import { motion, AnimatePresence } from "motion/react"

// Next.js Server Components — use the client export
import * as motion from "motion/react-client"

// Legacy import (still works with the framer-motion package)
import { motion } from "framer-motion"
如果项目已将
framer-motion
作为依赖项,为保持一致性请继续使用
"framer-motion"
导入,不要混合不同的导入来源。

Core Concepts

核心概念

motion.* Components

motion.* 组件

Every HTML/SVG element has a
motion
counterpart. These accept animation props:
tsx
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.5 }}
/>
Important:
motion.*
components are client components. In Next.js App Router, either mark the file
"use client"
or wrap them in a client component.
每个HTML/SVG元素都有对应的
motion
组件变体,这些组件支持动画属性:
tsx
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.5 }}
/>
重要提示:
motion.*
组件是客户端组件。在Next.js App Router中,需为文件标记
"use client"
或将其包裹在客户端组件中。

MotionValues — Animate Without Re-renders

MotionValues — 无重渲染动画

useMotionValue
creates values that update without triggering React re-renders. This is the key to 60fps animations:
tsx
const x = useMotionValue(0)
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])

return <motion.div style={{ x, opacity }} drag="x" />
Rules:
  • Use
    useMotionValue
    for any value that changes every frame (scroll position, drag position, continuous animation)
  • Use
    useTransform
    to derive values from other MotionValues (no re-renders)
  • Never use
    useState
    for frame-by-frame updates — it causes re-renders on every frame
useMotionValue
创建的数值更新时不会触发React重渲染,这是实现60fps流畅动画的关键:
tsx
const x = useMotionValue(0)
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])

return <motion.div style={{ x, opacity }} drag="x" />
规则:
  • 对于每帧都会变化的数值(滚动位置、拖拽位置、连续动画),使用
    useMotionValue
  • 使用
    useTransform
    从其他MotionValues派生新数值(无重渲染)
  • 绝不要使用
    useState
    处理逐帧更新——这会导致每帧都触发重渲染

Transitions

过渡效果

Control how animations move between states:
tsx
// Spring (default for physical properties like x, y, scale)
transition={{ type: "spring", stiffness: 300, damping: 30 }}

// Tween (default for non-physical properties like opacity, color)
transition={{ type: "tween", duration: 0.3, ease: "easeInOut" }}

// Custom cubic bezier
transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }}
控制动画在不同状态间的过渡方式:
tsx
// Spring (默认用于x、y、scale等物理属性)
transition={{ type: "spring", stiffness: 300, damping: 30 }}

// Tween (默认用于opacity、color等非物理属性)
transition={{ type: "tween", duration: 0.3, ease: "easeInOut" }}

// 自定义三次贝塞尔曲线
transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }}

Common Patterns

常见模式

Scroll-Triggered Fade-In

滚动触发淡入动画

The most common animation pattern. Use
whileInView
— do NOT manually use IntersectionObserver:
tsx
<motion.div
  initial={{ opacity: 0, y: 24 }}
  whileInView={{ opacity: 1, y: 0 }}
  viewport={{ once: true, amount: 0.1 }}
  transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
>
  {children}
</motion.div>
  • viewport.once: true
    — only animate on first entry (standard for marketing pages)
  • viewport.amount
    — how much of the element must be visible (0.1 = 10%)
  • viewport.margin
    — extend the trigger area (e.g.,
    "0px 0px 50px 0px"
    )
最常见的动画模式。使用
whileInView
——不要手动使用IntersectionObserver:
tsx
<motion.div
  initial={{ opacity: 0, y: 24 }}
  whileInView={{ opacity: 1, y: 0 }}
  viewport={{ once: true, amount: 0.1 }}
  transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
>
  {children}
</motion.div>
  • viewport.once: true
    — 仅在首次进入视图时触发动画(营销页面标准配置)
  • viewport.amount
    — 元素需可见的比例(0.1 = 10%)
  • viewport.margin
    — 扩展触发区域(例如:
    "0px 0px 50px 0px"

Staggered Children

子元素 stagger 动画

Animate children one after another using
variants
:
tsx
const container = {
  hidden: {},
  show: { transition: { staggerChildren: 0.1 } },
}

const item = {
  hidden: { opacity: 0, y: 20 },
  show: { opacity: 1, y: 0 },
}

<motion.ul variants={container} initial="hidden" whileInView="show" viewport={{ once: true }}>
  {items.map((i) => (
    <motion.li key={i} variants={item}>{i}</motion.li>
  ))}
</motion.ul>
Variants propagate — parent state changes flow to children automatically.
使用
variants
实现子元素依次动画:
tsx
const container = {
  hidden: {},
  show: { transition: { staggerChildren: 0.1 } },
}

const item = {
  hidden: { opacity: 0, y: 20 },
  show: { opacity: 1, y: 0 },
}

<motion.ul variants={container} initial="hidden" whileInView="show" viewport={{ once: true }}>
  {items.map((i) => (
    <motion.li key={i} variants={item}>{i}</motion.li>
  ))}
</motion.ul>
Variants会自动传播——父组件的状态变化会自动传递给子组件。

Exit Animations with AnimatePresence

结合AnimatePresence实现退出动画

Wrap conditionally rendered elements to animate them out before removal:
tsx
<AnimatePresence>
  {isOpen && (
    <motion.div
      key="modal"
      initial={{ opacity: 0, scale: 0.95 }}
      animate={{ opacity: 1, scale: 1 }}
      exit={{ opacity: 0, scale: 0.95 }}
      transition={{ duration: 0.2 }}
    />
  )}
</AnimatePresence>
Rules:
  • Children must have a unique
    key
  • exit
    prop defines the exit animation
  • Use
    mode="wait"
    to finish exit before entering next element
  • Use
    mode="popLayout"
    for layout-aware transitions
包裹条件渲染的元素,使其在移除前先执行退出动画:
tsx
<AnimatePresence>
  {isOpen && (
    <motion.div
      key="modal"
      initial={{ opacity: 0, scale: 0.95 }}
      animate={{ opacity: 1, scale: 1 }}
      exit={{ opacity: 0, scale: 0.95 }}
      transition={{ duration: 0.2 }}
    />
  )}
</AnimatePresence>
规则:
  • 子元素必须拥有唯一的
    key
  • exit
    属性定义退出动画
  • 使用
    mode="wait"
    等待退出动画完成后再进入下一个元素
  • 使用
    mode="popLayout"
    实现布局感知的过渡

Scroll-Linked Animations

滚动关联动画

Tie animation directly to scroll position:
tsx
const { scrollYProgress } = useScroll()
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0])
const scale = useTransform(scrollYProgress, [0, 1], [1, 0.8])

return <motion.div style={{ opacity, scale }} />
For element-specific scroll tracking:
tsx
const ref = useRef(null)
const { scrollYProgress } = useScroll({
  target: ref,
  offset: ["start end", "end start"], // when element enters/exits viewport
})
将动画直接绑定到滚动位置:
tsx
const { scrollYProgress } = useScroll()
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0])
const scale = useTransform(scrollYProgress, [0, 1], [1, 0.8])

return <motion.div style={{ opacity, scale }} />
针对特定元素的滚动追踪:
tsx
const ref = useRef(null)
const { scrollYProgress } = useScroll({
  target: ref,
  offset: ["start end", "end start"], // 元素进入/离开视口时
})

Smooth Spring Values

平滑Spring数值

Use
useSpring
for buttery-smooth transitions of MotionValues:
tsx
const scrollY = useMotionValue(0)
const smoothY = useSpring(scrollY, { stiffness: 100, damping: 30 })
使用
useSpring
实现MotionValues的丝滑过渡:
tsx
const scrollY = useMotionValue(0)
const smoothY = useSpring(scrollY, { stiffness: 100, damping: 30 })

Continuous Animation (useAnimationFrame)

连续动画(useAnimationFrame)

For animations that run every frame (gradient shimmer, counting, etc.):
tsx
const progress = useMotionValue(0)

useAnimationFrame((time, delta) => {
  const newValue = (time / 1000) % 100
  progress.set(newValue)
})

const backgroundPosition = useTransform(progress, (p) => `${p}% 50%`)

return <motion.span style={{ backgroundPosition }} />
适用于每帧都运行的动画(渐变闪光、数字计数等):
tsx
const progress = useMotionValue(0)

useAnimationFrame((time, delta) => {
  const newValue = (time / 1000) % 100
  progress.set(newValue)
})

const backgroundPosition = useTransform(progress, (p) => `${p}% 50%`)

return <motion.span style={{ backgroundPosition }} />

Layout Animations

布局动画

Animate layout changes automatically:
tsx
// Simple layout animation
<motion.div layout />

// Shared layout animation between components
<motion.div layoutId="hero-image" />
When the same
layoutId
exists in two different components, Motion animates between them (e.g., thumbnail to full-screen).
自动为布局变化添加动画:
tsx
// 简单布局动画
<motion.div layout />

// 组件间共享布局动画
<motion.div layoutId="hero-image" />
当两个不同组件拥有相同的
layoutId
时,Motion会自动在它们之间创建过渡动画(例如:缩略图到全屏)。

Gesture Animations

手势动画

tsx
<motion.button
  whileHover={{ scale: 1.05 }}
  whileTap={{ scale: 0.97 }}
  whileFocus={{ outline: "2px solid #5227FF" }}
  transition={{ type: "spring", stiffness: 400, damping: 17 }}
/>
tsx
<motion.button
  whileHover={{ scale: 1.05 }}
  whileTap={{ scale: 0.97 }}
  whileFocus={{ outline: "2px solid #5227FF" }}
  transition={{ type: "spring", stiffness: 400, damping: 17 }}
/>

Drag

拖拽功能

tsx
<motion.div
  drag           // enable both axes
  drag="x"       // constrain to x-axis
  dragConstraints={{ left: -100, right: 100 }}
  dragElastic={0.2}
  onDragEnd={(e, info) => {
    if (info.offset.x > 100) handleSwipeRight()
  }}
/>
tsx
<motion.div
  drag           // 启用双向拖拽
  drag="x"       // 限制为X轴拖拽
  dragConstraints={{ left: -100, right: 100 }}
  dragElastic={0.2}
  onDragEnd={(e, info) => {
    if (info.offset.x > 100) handleSwipeRight()
  }}
/>

Accessibility

无障碍访问

useReducedMotion

useReducedMotion

Always respect the user's motion preferences. This is not optional — it's an accessibility requirement:
tsx
import { useReducedMotion } from "framer-motion"

function AnimatedComponent({ children }) {
  const prefersReducedMotion = useReducedMotion()

  if (prefersReducedMotion) {
    return <div>{children}</div>
  }

  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      whileInView={{ opacity: 1, y: 0 }}
    >
      {children}
    </motion.div>
  )
}
Pattern: Check
useReducedMotion()
and either skip animation entirely or reduce it to opacity-only (no movement).
始终尊重用户的动效偏好。这不是可选项——而是无障碍访问的要求:
tsx
import { useReducedMotion } from "framer-motion"

function AnimatedComponent({ children }) {
  const prefersReducedMotion = useReducedMotion()

  if (prefersReducedMotion) {
    return <div>{children}</div>
  }

  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      whileInView={{ opacity: 1, y: 0 }}
    >
      {children}
    </motion.div>
  )
}
模式:检查
useReducedMotion()
,要么完全跳过动画,要么将动画简化为仅透明度变化(无位移)。

Performance Rules

性能规则

  1. Never animate
    width
    ,
    height
    ,
    top
    ,
    left
    — these trigger layout recalculation. Use
    transform
    properties instead (
    x
    ,
    y
    ,
    scale
    ,
    rotate
    ).
  2. Use MotionValues for frame-by-frame updates
    useState
    causes re-renders on every frame. MotionValues update the DOM directly.
  3. will-change: transform
    is added automatically by motion components — don't add it manually.
  4. layout
    animations are expensive
    — use them intentionally, not on every element.
  5. Avoid animating
    box-shadow
    — use
    filter: drop-shadow()
    or animate opacity of a pseudo-element shadow instead.
  6. Prefer
    opacity
    and
    transform
    — these are GPU-composited and run on a separate thread.
  7. Spring transitions are more natural than tween/easing for interactive elements (hover, tap, drag). Reserve tween for scroll-triggered entrances.
  1. 绝不要动画
    width
    height
    top
    left
    ——这些会触发布局重计算。改用
    transform
    属性(
    x
    y
    scale
    rotate
    )。
  2. 使用MotionValues处理逐帧更新——
    useState
    会导致每帧都触发重渲染。MotionValues直接更新DOM。
  3. **
    will-change: transform
    **会由motion组件自动添加——不要手动添加。
  4. layout
    动画开销较大
    ——要有针对性地使用,不要应用到每个元素。
  5. 避免动画
    box-shadow
    ——改用
    filter: drop-shadow()
    或为伪元素阴影添加透明度动画。
  6. 优先使用
    opacity
    transform
    ——这些由GPU合成,在单独线程运行。
  7. Spring过渡效果更自然,适用于交互元素(悬停、点击、拖拽)。Tween过渡效果适合滚动触发的入场动画。

Common Mistakes

常见错误

MistakeFix
useState
for drag position
useMotionValue
Manual
IntersectionObserver
whileInView
prop
setTimeout
for stagger
variants
with
staggerChildren
Missing
key
in
AnimatePresence
Add unique
key
to children
Animating in Server ComponentsAdd
"use client"
or use
motion/react-client
Ignoring reduced motionAlways check
useReducedMotion()
animate
on mount without
initial
Set
initial
to define start state
错误修复方案
使用
useState
存储拖拽位置
使用
useMotionValue
手动使用
IntersectionObserver
使用
whileInView
属性
使用
setTimeout
实现 stagger
使用带
staggerChildren
variants
AnimatePresence
中缺少
key
为子元素添加唯一
key
在服务端组件中使用动画添加
"use client"
或使用
motion/react-client
忽略减少动效的偏好始终检查
useReducedMotion()
挂载时使用
animate
但未设置
initial
设置
initial
定义初始状态

Recipes

示例代码

Progress Bar on Scroll

滚动进度条

tsx
const { scrollYProgress } = useScroll()
return (
  <motion.div
    className="fixed top-0 left-0 right-0 h-1 bg-primary origin-left z-50"
    style={{ scaleX: scrollYProgress }}
  />
)
tsx
const { scrollYProgress } = useScroll()
return (
  <motion.div
    className="fixed top-0 left-0 right-0 h-1 bg-primary origin-left z-50"
    style={{ scaleX: scrollYProgress }}
  />
)

Animated Counter

动画计数器

tsx
const count = useMotionValue(0)
const rounded = useTransform(count, (v) => Math.round(v))

useEffect(() => {
  const controls = animate(count, target, { duration: 2 })
  return controls.stop
}, [target])

return <motion.span>{rounded}</motion.span>
tsx
const count = useMotionValue(0)
const rounded = useTransform(count, (v) => Math.round(v))

useEffect(() => {
  const controls = animate(count, target, { duration: 2 })
  return controls.stop
}, [target])

return <motion.span>{rounded}</motion.span>

Page Transition (Next.js App Router)

页面过渡(Next.js App Router)

tsx
// template.tsx
"use client"
import { motion } from "framer-motion"

export default function Template({ children }) {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      transition={{ duration: 0.3 }}
    >
      {children}
    </motion.div>
  )
}
tsx
// template.tsx
"use client"
import { motion } from "framer-motion"

export default function Template({ children }) {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      transition={{ duration: 0.3 }}
    >
      {children}
    </motion.div>
  )
}

Animated Gradient Text (Shimmer Effect)

渐变文字动画(闪光效果)

tsx
const progress = useMotionValue(0)

useAnimationFrame((time) => {
  const duration = 8000
  const fullCycle = duration * 2
  const cycleTime = (time % fullCycle)
  if (cycleTime < duration) {
    progress.set((cycleTime / duration) * 100)
  } else {
    progress.set(100 - ((cycleTime - duration) / duration) * 100)
  }
})

const backgroundPosition = useTransform(progress, (p) => `${p}% 50%`)

return (
  <motion.span
    className="bg-clip-text text-transparent"
    style={{
      backgroundImage: "linear-gradient(to right, #5227FF, #a78bfa, #c084fc, #5227FF)",
      backgroundSize: "300% 100%",
      backgroundPosition,
    }}
  >
    {text}
  </motion.span>
)
tsx
const progress = useMotionValue(0)

useAnimationFrame((time) => {
  const duration = 8000
  const fullCycle = duration * 2
  const cycleTime = (time % fullCycle)
  if (cycleTime < duration) {
    progress.set((cycleTime / duration) * 100)
  } else {
    progress.set(100 - ((cycleTime - duration) / duration) * 100)
  }
})

const backgroundPosition = useTransform(progress, (p) => `${p}% 50%`)

return (
  <motion.span
    className="bg-clip-text text-transparent"
    style={{
      backgroundImage: "linear-gradient(to right, #5227FF, #a78bfa, #c084fc, #5227FF)",
      backgroundSize: "300% 100%",
      backgroundPosition,
    }}
  >
    {text}
  </motion.span>
)