学完这篇你能拿到一张可直接落地的选型表:知道什么场景用 CSS Transition、什么时候上 Framer Motion、什么时候必须请 GSAP,并且能避开三者最常见的坑。
React 里做动画,纠结的从来不是"能不能动",而是"用哪套方案不会在三个月后把自己坑死"。下面按步骤拆开讲。
第一步:先建立选型标准,别急着装包
在写第一行动画代码前,先回答四个问题:
- 动画是否由状态驱动(展开/收起、路由切换、列表增删)?是 → 声明式方案(CSS、Framer Motion)更省心。
- 是否需要中途打断、反向播放、时间轴串联?是 → GSAP 的
timeline 是唯一舒服的选择。
- 是否有滚动联动、拖拽、物理惯性?Framer Motion 的
useScroll、drag、spring 开箱即用。
- 是不是只改 opacity / transform?是 → 首选 CSS,别引入任何库。
注意:不要用"包体积"当唯一标准。gsap 压缩后约 70KB(gzip 后约 28KB),framer-motion 更大,但两者都能按需引入;真正的成本是"这套 API 团队里有没有人会改"。
第二步:CSS Transition —— 覆盖 70% 的日常需求
最简单也最快。核心思路:用 React 状态切 className,把动画完全交给 CSS。
function Panel({ open }) {
return (
<div className={`panel ${open ? 'panel--open' : ''}`}>
内容
</div>
);
}
.panel {
opacity: 0;
transform: translateY(8px);
transition: opacity .2s ease, transform .2s ease;
}
.panel--open {
opacity: 1;
transform: translateY(0);
}
只写 transition 属性(不是 animation),元素就一直留在 DOM 里,状态切换就是动画触发器。配合 prefers-reduced-motion 加一条媒体查询即可满足无障碍要求。
卸载时的退场动画怎么办? CSS 做不到——元素被 React 移除后没有机会播动画。要么自己维护一个 isClosing 状态延迟卸载,要么换成 Framer Motion 的 AnimatePresence。
注意:transition 只对可插值的属性生效。height: auto、display 都不能过渡。做高度展开请用 grid-template-rows: 0fr → 1fr 或 max-height 估一个上限值。
第三步:Framer Motion —— 状态驱动场景的主力
安装:npm i framer-motion(2024 年底后官方主推 npm i motion,导入路径改为 motion/react,API 基本一致)。
import { motion, AnimatePresence } from 'framer-motion';
<AnimatePresence>
{open && (
<motion.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 8 }}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
>
内容
</motion.div>
)}
</AnimatePresence>
三个 API 就能覆盖大多数需求:
initial / animate / exit:三段式声明,写起来就是"从哪来、到哪去、怎么走"。
AnimatePresence:解决卸载动画,这是它相对 CSS 的最大价值。
layout:列表增删排序时自动补间位置变化,一行属性搞定。
再做滚动进度条,用 useScroll + useTransform,不需要监听 scroll 事件手写计算。
注意:layout 属性会触发频繁的尺寸测量,长列表(几百项以上)谨慎使用,否则会明显掉帧。列表项多时给 layout 加 layoutScroll 或用 CSS 方案。
第四步:GSAP —— 时间轴、滚动叙事、复杂编排
安装:npm i gsap @gsap/react。
在 React 中务必用 useGSAP(或 useLayoutEffect + gsap.context)把动画作用域收进 ref,避免 StrictMode 双执行导致的重复动画和内存泄漏。
import { useRef } from 'react';
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';
function Hero() {
const box = useRef(null);
useGSAP(() => {
gsap.timeline()
.from('.title', { y: 30, opacity: 0, duration: .6 })
.from('.subtitle', { y: 20, opacity: 0, duration: .4 }, '-=0.3');
}, { scope: box });
return <div ref={box}>...</div>;
}
GSAP 的杀手锏是 timeline:把十几个元素的动画编排成一条时间线,-=0.3 控制重叠,seek() 拖动进度,timeScale() 调速度,暂停恢复都是原生能力。ScrollTrigger 做滚动视差、滚动钉住(pin)也远比自己算 scrollTop 可靠。3.13 起大部分原付费插件已免费商用,SplitText、ScrollSmoother 可用。
注意:GSAP 是命令式的,它直接改 DOM 样式,React 并不知道。所以不要让 GSAP 去改 React 也在控制的属性(比如受控的 className、style),否则会出现"动画播完又被 React 打回原样"。原则是:GSAP 负责的节点,交给它全权管。
第五步:按场景对号入座
| 场景 | 推荐方案 |
|---|
| 按钮 hover、菜单展开、简单淡入 | CSS Transition |
| 弹窗/抽屉/提示的进出场 | Framer Motion + AnimatePresence |
| 列表增删排序的位置补间 | Framer Motion layout |
| 拖拽、手势、弹簧物理 | Framer Motion |
| 落地页滚动叙事、多元素编排 | GSAP + ScrollTrigger |
| 需要暂停/倒放/进度拖动的复杂动画 | GSAP timeline |
实际项目里混用是常态:CSS 打底做微交互,Framer Motion 管组件状态动画,GSAP 只留给那一两个重点页面。按需 import() 动态引入,别全塞进首屏包。
注意:不管用哪套,动画属性尽量限制在 transform 和 opacity。改 width、height、top、left 会触发重排,在低端安卓机上直接变成幻灯片。
小结
- CSS Transition 是默认选项,改
className 触发,零依赖,覆盖大部分微交互;卸载动画是它的短板。
- Framer Motion 用
initial/animate/exit 三段式表达状态动画,AnimatePresence 解决退场,layout 解决列表补间,是 React 项目的主力。
- GSAP 用
useGSAP + scope 收口,timeline 做编排,ScrollTrigger 做滚动叙事,复杂场景无可替代。
- 判断顺序:先问"是否状态驱动",再问"是否需要时间轴/滚动",最后才考虑库体积。
- 性能红线只有一条:优先动
transform 和 opacity。