mcpField
R
react-animations

react-animations

github.com/fcsouza设计与 UI2444 次安装1 周前更新
代码仓库

关于 react-animations

面向 React 开发者的动画技能,指导选择并使用 Framer Motion、React Spring、GSAP 或 CSS/Tailwind。内容涵盖实现、无障碍与性能,并提供可复用组件和变体模板。

主要功能

  1. 01动画库选择建议
  2. 02Framer Motion 手势与布局模式
  3. 03滚动、拖拽和时间轴动画指南
  4. 04减少动态效果的无障碍实践
  5. 05性能与包体积建议
  6. 06可复用动画组件和变体

适用场景

  1. 01为 React 界面选择动画方案
  2. 02添加滚动触发或共享元素过渡
  3. 03检查动画的无障碍性和性能
  4. 04构建可复用的游戏 UI 动画

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/fcsouza/agent-skills --skill react-animations

安装后在 Claude Code 中输入/react-animations确认已加载。

附带文件boilerplate/motion-components.tsxtemplates/animation-variants.ts

详细介绍

React 动画

使用 Framer Motion(主要)、React Spring(物理效果)、GSAP(时间轴)和 CSS/Tailwind(简单场景),在 React 中实现适用于生产环境的高质量动画。

库选择表

使用场景库原因
入场/退场动画Framer MotionAnimatePresence 可处理卸载
共享元素过渡Framer MotionlayoutId
基于物理效果(弹簧、反弹)Framer Motion 或 React Spring弹簧配置
复杂时间轴/序列GSAPTimeline API
滚动触发Framer Motion(whileInView / useScroll)内置滚动钩子
简单的悬停/聚焦状态CSS Tailwind无需 JS
拖放Framer Motion内置手势支持
SVG 路径动画GSAP 或 Framer Motion两者都支持 SVG
命令式/程序化动画Framer Motion useAnimate现代命令式 API

核心原则

Matt Perry(Framer Motion 创作者):“动画应该以声明式方式定义,而不是以命令式方式管理。描述目标状态,其余工作交给库处理。” Sarah Drasner:“动画不是装饰,而是沟通。每个动态效果都应该有明确目的。”

  1. 简单情况用 CSS,复杂情况用 JS — 如果 Tailwind transition 能满足需求,就用它;不要为了悬停状态添加 Framer Motion
  2. 只对合成属性应用动画 — 使用 transform 和 opacity;不要使用 width、height、top、left(会导致重排)
  3. 用 AnimatePresence 包裹条件渲染 — 否则会跳过退场动画
  4. 用变体协调动画 — 在组件外部将动画状态定义为对象,不要使用内联值
  5. 用 layoutId 实现共享元素过渡 — Framer Motion 会处理位置之间的插值
  6. 手势驱动时使用 useMotionValue — 不要对驱动动画的值使用 useState
  7. 将帧率预算保持在 60fps — 不要在渲染周期中执行动画逻辑;使用变换

Framer Motion 核心模式

基本动画
<motion.div
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  transition={{ duration: 0.3 }}
/>
手势状态(whileHover、whileTap)
<motion.button
  whileHover={{ scale: 1.05 }}
  whileTap={{ scale: 0.95 }}
  transition={{ type: 'spring', stiffness: 400, damping: 17 }}
/>
带子元素交错效果的变体
const container = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: { staggerChildren: 0.1 },
  },
};

const item = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0 },
};

<motion.ul variants={container} initial="hidden" animate="visible">
  {items.map((i) => (
    <motion.li key={i} variants={item} />
  ))}
</motion.ul>
使用 AnimatePresence 实现退场动画

mode 属性控制进入和退出的元素如何交互:

  • "sync"(默认)— 进入和退出同时发生
  • "wait" — 退出完成后才开始进入(适用于页面过渡)
  • "popLayout" — 立即将退出元素从布局流中移除
<AnimatePresence mode="wait">
  {isVisible && (
    <motion.div
      key="modal"
      initial={{ opacity: 0, scale: 0.9 }}
      animate={{ opacity: 1, scale: 1 }}
      exit={{ opacity: 0, scale: 0.9 }}
    />
  )}
</AnimatePresence>
使用 whileInView 实现滚动触发动画

最简单的方法,无需滚动钩子:

<motion.div
  initial={{ opacity: 0, y: 40 }}
  whileInView={{ opacity: 1, y: 0 }}
  viewport={{ once: true, margin: '-100px' }}
  transition={{ duration: 0.5 }}
/>

设置 viewport.once: true,这样滚动回去时动画就不会重播。

使用 useScroll + useTransform 实现视差效果
const { scrollYProgress } = useScroll();
const y = useTransform(scrollYProgress, [0, 1], [0, -200]);

<motion.div style={{ y }} />
使用 layoutId 实现共享元素
// In list view
<motion.div layoutId={`card-${id}`}>
  <Thumbnail />
</motion.div>

// In detail view
<motion.div layoutId={`card-${id}`}>
  <FullImage />
</motion.div>
带约束的拖动
<motion.div
  drag
  dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
  dragElastic={0.2}
  whileDrag={{ scale: 1.1 }}
/>
useAnimate — 命令式动画(v11+)

对于程序化序列,优先使用 useAnimate,而不是 useAnimation。它的作用范围限定在 ref 内,并且可在该范围内与任意选择器配合使用:

const [scope, animate] = useAnimate();

const handleClick = async () => {
  await animate(scope.current, { scale: 1.2 }, { duration: 0.2 });
  await animate(scope.current, { scale: 1 }, { duration: 0.1 });
};

<div ref={scope}>
  <button onClick={handleClick}>Click me</button>
</div>
MotionConfig — 全局动画设置

包裹整个应用(或一个子树),即可设置减少动态效果或弹簧预设等默认选项:

<MotionConfig reducedMotion="user">
  <App />
</MotionConfig>

reducedMotion="user" 会自动为在操作系统级别设置了“减少动态效果”偏好的用户禁用动画。这是大规模实现无障碍支持最简单的方式。

LazyMotion — 优化包体积

对于生产环境中的应用,可将 motion 替换为 LazyMotion + m,以便对动画引擎进行代码分割:

import { LazyMotion, domAnimation, m } from 'framer-motion';

<LazyMotion features={domAnimation}>
  <m.div animate={{ opacity: 1 }} />
</LazyMotion>

如果需要拖动或布局动画,请使用 domMax,而不是 domAnimation。

游戏 UI 模式

平滑缓动生命值条
const motionWidth = useMotionValue(current / max);
const springWidth = useSpring(motionWidth, { stiffness: 200, damping: 30 });

useEffect(() => {
  motionWidth.set(Math.max(0, Math.min(1, current / max)));
}, [current, max]);

<motion.div style={{ scaleX: springWidth, transformOrigin: 'left' }} />
向上浮动的伤害数字
<motion.span
  initial={{ opacity: 1, y: 0 }}
  animate={{ opacity: 0, y: -60 }}
  transition={{ duration: 0.8, ease: 'easeOut' }}
  onAnimationComplete={onComplete}
>
  -{damage}
</motion.span>
卡片翻转(rotateY)
<motion.div animate={{ rotateY: isFlipped ? 180 : 0 }} style={{ perspective: 1000 }}>
  <div style={{ backfaceVisibility: 'hidden' }}>{front}</div>
  <div style={{ backfaceVisibility: 'hidden', rotateY: 180 }}>{back}</div>
</motion.div>
屏幕抖动(useAnimate)
const [scope, animate] = useAnimate();

const shake = async () => {
  await animate(scope.current, { x: [0, -10, 10, -10, 10, 0] }, { duration: 0.4 });
};

<div ref={scope}>{children}</div>
菜单滑入/滑出
<AnimatePresence>
  {isOpen && (
    <motion.nav
      initial={{ x: -300 }}
      animate={{ x: 0 }}
      exit={{ x: -300 }}
      transition={{ type: 'spring', stiffness: 300, damping: 30 }}
    />
  )}
</AnimatePresence>
物品栏物品掉落(弹簧物理效果)
<motion.div
  initial={{ y: -200, opacity: 0 }}
  animate={{ y: 0, opacity: 1 }}
  transition={{ type: 'spring', stiffness: 400, damping: 15 }}
/>

无障碍

始终尊重用户对动态效果的偏好。以下是两种实现方式:

1. MotionConfig(推荐用于应用) — 包裹整个组件树:

<MotionConfig reducedMotion="user">
  <App />
</MotionConfig>

2. useReducedMotion 钩子 — 用于组件级控制:

const shouldReduceMotion = useReducedMotion();

<motion.div
  animate={{ opacity: 1, y: shouldReduceMotion ? 0 : -20 }}
/>

性能

  • 仅对 transform 和 opacity 进行动画处理——由 GPU 合成,不会触发布局或绘制
  • 对始终执行动画的元素使用 will-change: transform(提升到独立图层)
  • 使用 motion.create(Component) 为自定义组件添加动画,无需包装 <div>
  • layout 属性会触发自动布局动画——在大型 DOM 树中开销较高;应将范围限定在尽可能小的子树中
  • 对驱动动画的值,优先使用 useMotionValue 而不是 useState——运动值不会触发重新渲染
  • 使用 LazyMotion 和 domAnimation,与完整包相比可减少约 15kb gzip 体积

设置

bun add framer-motion

无需配置——不需要任何 Provider。直接导入并使用 motion.div。

如需进行全局无障碍处理,请包装应用根节点:

import { MotionConfig } from 'framer-motion';

<MotionConfig reducedMotion="user">
  <App />
</MotionConfig>

样板代码

boilerplate/motion-components.tsx — 可直接使用的组件:FadeIn、SlideIn、ScaleIn、StaggerList、FloatingNumber、HealthBar、AnimatedCard

templates/animation-variants.ts — 可复用的变体对象和弹簧过渡预设

交叉引用

  • vercel-react-best-practices — React 性能模式
  • ui-ux-game — 游戏 HUD 和 UI 模式
  • frontend-design — 组件设计和 Tailwind 模式

常见问题与反模式

  • 为会触发布局的属性添加动画(width、height、top、left)——改用 scaleX/scaleY 或 layout 属性
  • 使用 exit 属性时忘记 AnimatePresence——没有包装器时,退出动画会被静默跳过
  • 在渲染期间创建运动值——使用 useMotionValue Hook;在渲染期间创建会导致内存泄漏
  • 在同一元素上同时使用 CSS transition 和 Framer Motion——二者会冲突;选择其中一种
  • 过度使用动画——每次交互都添加动画会造成感官过载;动画应传达信息,而非仅作装饰
  • 挂载时添加动画却未设置 initial——组件会在动画开始前闪现;务必设置 initial
  • 使用 useAnimation——已弃用;需要命令式动画时改用 useAnimate
  • 大型 layout 动画——在深层嵌套树中使用 layout 属性会导致昂贵的重新计算;应将范围限定在尽可能小的子树中
  • 忽略无障碍功能——务必使用 MotionConfig reducedMotion="user" 或 useReducedMotion

来源

  • Framer Motion 文档 — https://motion.dev
  • Matt Perry — Framer Motion 创作者,API 设计演讲
  • Sarah Drasner —《SVG 动画》,动画设计模式
  • GSAP 文档 — https://gsap.com

评论

支持 Markdown