先问:能用 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 上限——超过就感觉慢。

性能优化

动画必看的两条:

  • 只动 transformopacity——其他属性触发 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 实战