CSS transition / keyframes 能解决简单动画,复杂的(列表重排、组件进入/离开、
gesture、SVG morph)就力不从心。Framer Motion 是 React 生态的事实标准动画库,
API 简洁,自动 GPU 加速。
安装
npm i framer-motion
1. 最简单的进入动画
import { motion } from 'framer-motion'
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
Hello
</motion.div>
任何 HTML 元素加 motion. 前缀即可。
2. 进入 + 离开(AnimatePresence)
import { motion, AnimatePresence } from 'framer-motion'
<AnimatePresence>
{visible && (
<motion.div
key="modal"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
>
Modal
</motion.div>
)}
</AnimatePresence>
AnimatePresence 在子元素被 React 卸载时延迟卸载,让 exit 动画完成。
必须给每个直接子元素显式 key。
3. 列表动画(重排 / 添加 / 删除)
<AnimatePresence>
{items.map(item => (
<motion.li
key={item.id}
layout // 自动 FLIP 动画
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
>
{item.text}
</motion.li>
))}
</AnimatePresence>
layout 让元素位置变化时自动 morph(FLIP 算法)。
增删 / 排序列表时无需手动算位移。
4. variants:复用动画状态
const variants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
}
<motion.div variants={variants} initial="hidden" animate="visible">
...
</motion.div>
// 父子联动
const parent = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { staggerChildren: 0.1 } },
}
const child = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
}
<motion.ul variants={parent} initial="hidden" animate="visible">
{items.map(i => <motion.li key={i.id} variants={child}>{i.text}</motion.li>)}
</motion.ul>
staggerChildren: 0.1 让子元素一个接一个延迟 0.1s 进入,列表波浪效果。
5. 手势:hover / tap / drag
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Click
</motion.button>
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: 0, bottom: 0 }}
dragElastic={0.5}
whileDrag={{ scale: 1.1 }}
>
拖我
</motion.div>
drag 自动处理触摸 / 鼠标拖动。dragConstraints 限制范围。
6. transition 选项
<motion.div
animate={{ x: 100 }}
transition={{
duration: 0.5,
ease: 'easeOut', // 'linear' | 'easeIn' | 'easeInOut' | 'circIn' | ...
// 或弹簧物理
type: 'spring',
stiffness: 100,
damping: 10,
}}
/>
弹簧(spring)参数比 duration 更自然,是 Framer Motion 默认。
7. scroll-triggered 动画
import { motion, useScroll, useTransform } from 'framer-motion'
function Hero() {
const { scrollYProgress } = useScroll()
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0])
const y = useTransform(scrollYProgress, [0, 1], [0, -200])
return <motion.h1 style={{ opacity, y }}>Title</motion.h1>
}
useTransform 把 0-1 的 scrollYProgress 映射到任意值范围,做视差效果。
8. layout 高级:跨组件共享
import { motion } from 'framer-motion'
// 小卡片
<motion.div layoutId="card-1" onClick={open}>
<img src="thumb.jpg" />
</motion.div>
// 弹出全屏
{open && (
<motion.div layoutId="card-1" className="fullscreen">
<img src="full.jpg" />
</motion.div>
)}
两个不同位置的元素 layoutId 相同 → Framer 自动 morph 从一个位置到另一个。
"卡片展开"效果一行写完。
9. SVG morph
<motion.path
d={pathData}
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 2 }}
/>
pathLength: 0 → 1 让 SVG 路径"画出来"效果。
10. 性能注意
Framer Motion 默认用 transform / opacity(GPU 友好)。不要动 width / height /
top / left(CPU 重排)。
// ❌ width 变化触发 layout
<motion.div animate={{ width: 200 }} />
// ✅ transform scale
<motion.div animate={{ scaleX: 2 }} />
11. 与 Tailwind / CSS-in-JS
motion.div 接受所有 HTML props,class / style 正常写:
<motion.div
className="bg-blue-500 rounded p-4"
whileHover={{ scale: 1.05 }}
/>
12. 替代方案
- react-spring:物理为主,API 不同
- gsap:传统 JS 动画,强大但非 React-first
- Motion One:framer-motion 作者的"无 React 依赖"版
React 项目就用 Framer Motion,最省事。
踩过的坑
- AnimatePresence 子元素必须有
key:忘了 key 看不到 exit 动画。 - 直接给
<div>加 motion 属性不工作:必须用motion.div。 - 大量元素同时 layout 动画 → 卡。
layout只给真正需要 morph 的元素加。 - prefers-reduced-motion:尊重用户系统设置,给敏感动画加:
tsx const reduce = useReducedMotion() <motion.div animate={{ x: reduce ? 0 : 100 }} />
登录后参与评论。