你是不是也遇到过这种情况:用 CSS 的transition做动画,简单的展开收起还行,但稍微复杂点的交互(比如列表添加删除动画、拖拽效果)就捉襟见肘,代码写得又长又乱?
其实,React 生态中有个专门解决动画问题的 “神器”——framer-motion。它能让你用几行代码实现原本需要几十行 CSS 才能搞定的动画,甚至能轻松实现拖拽、手势等复杂交互,让你的 React 应用从此告别 “PPT 式切换”。
CSS transition 的局限
用 CSS 的transition做简单动画确实方便,比如这个点击展开 / 收起的盒子:
// components/Box.jsx
import { useState } from 'react';
import styles from './box.module.css';
const Box = () => {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen(!open)}>
{open ? '收起' : '展开'}
</button>
{/* 用CSS类名切换控制高度,配合transition实现动画 */}
<div className={`${styles.box} ${open ? styles.open : ''}`} />
</div>
);
};
/* box.module.css */
.box {
width: 100px;
height: 0;
background: lightblue;
transition: height 0.3s ease; /* 定义过渡效果 */
overflow: hidden;
}
.box.open {
height: 100px; /* 展开时的高度 */
}
这个动画能跑起来,但如果需求变复杂,CSS 就很难应对了:
- 想要 “先变宽再变高” 的序列动画,CSS 需要配合
animation和关键帧,代码冗长; - 想要列表项删除时 “渐隐 + 上移” 的联动效果,CSS 几乎无法实现;
- 想要给动画加 “弹性”“弹跳” 等物理效果,CSS 的
ease函数不够用。
这时候,framer-motion的优势就体现出来了。
framer-motion:让 React 动画变得像写 JSX 一样简单
framer-motion是 React 生态中最流行的动画库之一,它的核心特点是声明式 API—— 你只需要描述 “动画的开始状态、结束状态和过渡方式”,剩下的交给库来处理。
(1)基础用法:从 “入场动画” 开始
先安装依赖:
pnpm i framer-motion
用framer-motion实现一个 “元素挂载时渐入 + 上移” 的动画:
// components/MotionBox.jsx
import { motion } from 'framer-motion';
const MotionBox = () => {
return (
// 用motion组件包裹需要动画的元素
<motion.div
// 初始状态:透明度0,Y轴偏移-50px(上方)
initial={{ opacity: 0, y: -50 }}
// 目标状态:透明度1,Y轴偏移0(正常位置)
animate={{ opacity: 1, y: 0 }}
// 过渡配置:持续0.5秒,带弹性效果
transition={{ duration: 0.5, type: 'spring', stiffness: 100 }}
style={{ background: 'skyblue', padding: '20px' }}
>
<h2>我是带动画的盒子</h2>
</motion.div>
);
};
核心属性解析:
motion:framer-motion 提供的 “动画化组件”,可以替代普通的div、span等,支持所有 HTML 标签(如motion.button、motion.ul)。initial:元素初始状态(未动画时),通常用于定义 “入场前” 的状态。animate:元素的目标状态,元素挂载后会自动从initial过渡到animate。transition:过渡配置,可指定动画时长(duration)、缓动类型(type: 'spring'表示弹性效果)等。
(2)状态切换动画:比 CSS 类名切换更灵活
用framer-motion实现 “点击展开 / 收起”,支持更复杂的状态变化:
// components/AnimatedBox.jsx
import { useState } from 'react';
import { motion } from 'framer-motion';
const AnimatedBox = () => {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen(!open)}>
{open ? '收起' : '展开'}
</button>
{/* 用motion.div实现高度+背景色的联动动画 */}
<motion.div
// 初始状态:高度0,背景色浅蓝
initial={{ height: 0, backgroundColor: '#add8e6' }}
// 根据open状态动态切换目标状态
animate={{
height: open ? 100 : 0,
backgroundColor: open ? '#f5f5f5' : '#add8e6'
}}
// 过渡配置:0.3秒,easeOut缓动
transition={{ duration: 0.3, ease: 'easeOut' }}
style={{ overflow: 'hidden' }}
/>
</div>
);
};
这个例子中,framer-motion自动处理了 “高度” 和 “背景色” 的同步过渡,无需像 CSS 那样手动写两个transition。
(3)进阶:用variants组织复杂状态
当动画状态较多时(如 “默认”“hover”“激活”),可以用variants统一管理状态,让代码更清晰:
// components/ButtonWithVariants.jsx
import { motion } from 'framer-motion';
// 定义动画变体(不同状态的样式)
const variants = {
default: { scale: 1, backgroundColor: '#fff' }, // 默认状态
hover: { scale: 1.05, backgroundColor: '#f0f0f0' }, // hover状态
active: { scale: 0.95, backgroundColor: '#e0e0e0' } // 点击状态
};
const AnimatedButton = () => {
return (
<motion.button
variants={variants} // 关联变体
initial="default" // 初始状态对应variants.default
whileHover="hover" // hover时自动切换到hover状态
whileTap="active" // 点击时自动切换到active状态
transition={{ type: 'spring', stiffness: 300 }} // 过渡配置
style={{
padding: '10px 20px',
border: '1px solid #ddd',
borderRadius: '4px',
cursor: 'pointer'
}}
>
点我试试
</motion.button>
);
};
variants的优势在于:状态逻辑与 UI 分离,方便复用和维护。
(4)列表动画:删除项时的 “联动效果”
用framer-motion的AnimatePresence组件,可以轻松实现 “组件卸载时的动画”,这在列表中非常实用:
// components/TodoList.jsx
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
const TodoList = () => {
const [todos, setTodos] = useState([
{ id: 1, text: '学习framer-motion' },
{ id: 2, text: '实现列表动画' }
]);
const removeTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
{/* AnimatePresence用于检测子元素的挂载/卸载 */}
<AnimatePresence>
{todos.map(todo => (
<motion.div
key={todo.id}
// 入场动画:从下方滑入,渐显
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
// 离场动画:渐隐,向右滑出
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.2 }}
style={{
padding: '10px',
border: '1px solid #eee',
margin: '5px 0',
display: 'flex',
justifyContent: 'space-between'
}}
>
<span>{todo.text}</span>
<button onClick={() => removeTodo(todo.id)}>删除</button>
</motion.div>
))}
</AnimatePresence>
</div>
);
};
当删除列表项时,被删除的项会 “渐隐 + 右滑” 消失,剩下的项会自动上移补位,整个过程流畅自然 —— 这用 CSS 几乎无法实现。
(5)物理效果:让动画更 “真实”
framer-motion支持多种物理动画类型(如spring弹簧、inertia惯性),让动画更贴近真实世界的运动规律:
// components/BouncyBox.jsx
import { motion } from 'framer-motion';
const BouncyBox = () => {
return (
<motion.div
// 初始状态:Y轴偏移-200px(上方)
initial={{ y: -200 }}
// 目标状态:Y轴偏移0(落下)
animate={{ y: 0 }}
// 弹簧效果:stiffness(刚度)越小,弹跳越明显
transition={{ type: 'spring', stiffness: 50, damping: 10 }}
style={{
width: 100,
height: 100,
backgroundColor: 'pink',
borderRadius: '8px'
}}
/>
);
};
这个盒子会像 “弹簧球” 一样落下并轻微弹跳,比 CSS 的
ease效果生动得多。
入门必知:Framer Motion 核心概念
motion组件:替代普通 HTML 标签(div→motion.div,button→motion.button),所有动画属性都加在这上面;initial:初始状态(动画开始前的样式);animate:目标状态(动画要达到的样式);transition:过渡规则(时长、曲线、延迟等);variants:动画变体,集中管理多个状态的样式,方便复用;AnimatePresence:处理元素 “退场” 动画(必须包裹动态增删的元素)。
总结:从 CSS 到 framer-motion 的选择指南
- 简单的属性过渡(如 hover 时颜色变化):用 CSS
transition足够,轻量高效; - 复杂的状态切换、序列动画、列表动画:用
framer-motion,节省开发时间; - 需要物理效果、手势交互:必选
framer-motion,没有更简单的方案。