先问:能用 CSS 吗
.fade-in { animation: fadeIn 0.3s ease; }
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
CSS 动画走 GPU、不阻塞 JS、不进 React re-render——永远先考虑。
简单的 hover、淡入、滑动——CSS / Tailwind 的 animate-*、transition-* 都搞定。
// Tailwind
<div className="transition-transform duration-300 hover:scale-105">...</div>
<div className="animate-pulse bg-zinc-200 h-8 w-32 rounded" /> // 骨架屏
进阶:Framer Motion
CSS 搞不定的:依赖 state、需要物理感、列表入场动画、拖拽。装 Framer Motion:
npm i framer-motion
基本
import { motion } from 'framer-motion';
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
Hello
</motion.div>
退场动画(AnimatePresence)
import { AnimatePresence, motion } from 'framer-motion';
<AnimatePresence>
{open && (
<motion.div
key="modal"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Modal
</motion.div>
)}
</AnimatePresence>
React 默认元素一旦 unmount 就没了——AnimatePresence 等 exit 动画结束才真的移除。
列表动画
<AnimatePresence>
{items.map(item => (
<motion.div
key={item.id}
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{item.name}
</motion.div>
))}
</AnimatePresence>
layout prop 自动处理位置变化的过渡(FLIP 技术)——元素位置变了平滑滑过去。
共享元素动画(layoutId)
{open ? (
<motion.div layoutId="card" className="big" />
) : (
<motion.div layoutId="card" className="small" />
)}
同 layoutId 的元素在切换时自动过渡,不论 DOM 位置变多远。
CSS View Transitions API(2024 起浏览器原生)
document.startViewTransition(() => {
// 改 DOM
setView('detail');
});
::view-transition-old(root) { animation: fade-out 0.3s; }
::view-transition-new(root) { animation: fade-in 0.3s; }
原生跨页面动画——首选支持的场景。Next.js 15 实验性 unstable_ViewTransition 包装。
兼容性:现代 Chrome/Edge/Safari 都支持,Firefox 跟进中。不要作为唯一动画手段,没支持的浏览器要回退。
React Spring
npm i @react-spring/web
物理动画为主(弹簧效果)。比 Framer Motion 数学上更"真实",但 API 学习成本高。Framer Motion 在 2024+ 更主流。
反模式
❌ 用 React state 做每一帧动画:
const [x, setX] = useState(0);
requestAnimationFrame(() => setX(x + 1)); // 每帧 re-render,卡爆
✅ 用 motion / CSS / requestAnimationFrame + 直接改 ref style:
const ref = useRef<HTMLDivElement>(null);
const raf = () => {
if (ref.current) ref.current.style.transform = `translateX(${x}px)`;
requestAnimationFrame(raf);
};
❌ 滥用动画:让产品看起来"现代"反而拖累交互速度。Material Design 建议单次动画 200-300ms 上限——超过就感觉慢。
性能优化
动画必看的两条:
- 只动
transform和opacity——其他属性触发 layout / paint,慢 will-change: transform让浏览器提前 GPU 化(不要乱加,会吃显存)
选型表
| 场景 | 用 |
|---|---|
| hover / focus | CSS transition |
| 静态入场 | CSS animation |
| 骨架屏 | Tailwind animate-pulse |
| 列表增删过渡 | Framer Motion AnimatePresence |
| 拖拽 / 物理 | Framer Motion |
| 跨页面切换 | View Transitions API |
| 极端性能场景 | 直接操作 DOM transform |
→ 下一篇 TypeScript 实战