framer-motion
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMotion (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 to . Both package names work, but the import path matters:
framer-motionmotiontsx
// 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 as a dependency, keep using imports for consistency. Don't mix import sources.
framer-motion"framer-motion"Motion已从更名为。两个包名均有效,但导入路径有讲究:
framer-motionmotiontsx
// 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 counterpart. These accept animation props:
motiontsx
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
/>Important: components are client components. In Next.js App Router, either mark the file or wrap them in a client component.
motion.*"use client"每个HTML/SVG元素都有对应的组件变体,这些组件支持动画属性:
motiontsx
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
/>重要提示: 组件是客户端组件。在Next.js App Router中,需为文件标记或将其包裹在客户端组件中。
motion.*"use client"MotionValues — Animate Without Re-renders
MotionValues — 无重渲染动画
useMotionValuetsx
const x = useMotionValue(0)
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])
return <motion.div style={{ x, opacity }} drag="x" />Rules:
- Use for any value that changes every frame (scroll position, drag position, continuous animation)
useMotionValue - Use to derive values from other MotionValues (no re-renders)
useTransform - Never use for frame-by-frame updates — it causes re-renders on every frame
useState
useMotionValuetsx
const x = useMotionValue(0)
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])
return <motion.div style={{ x, opacity }} drag="x" />规则:
- 对于每帧都会变化的数值(滚动位置、拖拽位置、连续动画),使用
useMotionValue - 使用从其他MotionValues派生新数值(无重渲染)
useTransform - 绝不要使用处理逐帧更新——这会导致每帧都触发重渲染
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 — do NOT manually use IntersectionObserver:
whileInViewtsx
<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>- — only animate on first entry (standard for marketing pages)
viewport.once: true - — how much of the element must be visible (0.1 = 10%)
viewport.amount - — extend the trigger area (e.g.,
viewport.margin)"0px 0px 50px 0px"
最常见的动画模式。使用——不要手动使用IntersectionObserver:
whileInViewtsx
<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 - — 元素需可见的比例(0.1 = 10%)
viewport.amount - — 扩展触发区域(例如:
viewport.margin)"0px 0px 50px 0px"
Staggered Children
子元素 stagger 动画
Animate children one after another using :
variantstsx
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.
使用实现子元素依次动画:
variantstsx
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 - prop defines the exit animation
exit - Use to finish exit before entering next element
mode="wait" - Use for layout-aware transitions
mode="popLayout"
包裹条件渲染的元素,使其在移除前先执行退出动画:
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 for buttery-smooth transitions of MotionValues:
useSpringtsx
const scrollY = useMotionValue(0)
const smoothY = useSpring(scrollY, { stiffness: 100, damping: 30 })使用实现MotionValues的丝滑过渡:
useSpringtsx
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 exists in two different components, Motion animates between them (e.g., thumbnail to full-screen).
layoutId自动为布局变化添加动画:
tsx
// 简单布局动画
<motion.div layout />
// 组件间共享布局动画
<motion.div layoutId="hero-image" />当两个不同组件拥有相同的时,Motion会自动在它们之间创建过渡动画(例如:缩略图到全屏)。
layoutIdGesture 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 and either skip animation entirely or reduce it to opacity-only (no movement).
useReducedMotion()始终尊重用户的动效偏好。这不是可选项——而是无障碍访问的要求:
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
性能规则
-
Never animate,
width,height,top— these trigger layout recalculation. Useleftproperties instead (transform,x,y,scale).rotate -
Use MotionValues for frame-by-frame updates —causes re-renders on every frame. MotionValues update the DOM directly.
useState -
is added automatically by motion components — don't add it manually.
will-change: transform -
animations are expensive — use them intentionally, not on every element.
layout -
Avoid animating— use
box-shadowor animate opacity of a pseudo-element shadow instead.filter: drop-shadow() -
Preferand
opacity— these are GPU-composited and run on a separate thread.transform -
Spring transitions are more natural than tween/easing for interactive elements (hover, tap, drag). Reserve tween for scroll-triggered entrances.
-
绝不要动画、
width、height、top——这些会触发布局重计算。改用left属性(transform、x、y、scale)。rotate -
使用MotionValues处理逐帧更新——会导致每帧都触发重渲染。MotionValues直接更新DOM。
useState -
****会由motion组件自动添加——不要手动添加。
will-change: transform -
动画开销较大——要有针对性地使用,不要应用到每个元素。
layout -
避免动画——改用
box-shadow或为伪元素阴影添加透明度动画。filter: drop-shadow() -
优先使用和
opacity——这些由GPU合成,在单独线程运行。transform -
Spring过渡效果更自然,适用于交互元素(悬停、点击、拖拽)。Tween过渡效果适合滚动触发的入场动画。
Common Mistakes
常见错误
| Mistake | Fix |
|---|---|
| |
Manual | |
| |
Missing | Add unique |
| Animating in Server Components | Add |
| Ignoring reduced motion | Always check |
| Set |
| 错误 | 修复方案 |
|---|---|
使用 | 使用 |
手动使用 | 使用 |
使用 | 使用带 |
| 为子元素添加唯一 |
| 在服务端组件中使用动画 | 添加 |
| 忽略减少动效的偏好 | 始终检查 |
挂载时使用 | 设置 |
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>
)