mcpField
W
web-animation-framer-motion

web-animation-framer-motion

github.com/agents-inc设计与 UI26122 次安装2 天前更新
代码仓库

关于 web-animation-framer-motion

面向 React 开发者的 Motion 动画模式指南,介绍如何为组件状态、布局变化和滚动添加动画。内容涵盖变体编排、进出场、手势及减少动态效果的支持。

主要功能

  1. 01使用 motion 组件定义动画状态
  2. 02用变体编排多个子元素
  3. 03为组件离场运行动画
  4. 04动画化布局变化与共享元素
  5. 05基于滚动和手势驱动动画
  6. 06支持减少动态效果偏好

适用场景

  1. 01为条件渲染的组件添加进出场效果
  2. 02编排列表项目的交错动画
  3. 03为标签或卡片布局变化添加动画
  4. 04制作滚动触发或手势驱动效果

安装

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

$npx skills add https://github.com/agents-inc/skills --skill web-animation-framer-motion

安装后在 Claude Code 中输入/web-animation-framer-motion确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.yaml
reference.md
examples/
core.md
layout.md
scroll.md
sequences.md
svg.md

详细介绍

动效模式

快速指南: motion.* 组件接收 initial、animate、exit 和 transition 属性; variants 会将这些状态提取为命名集合,供父组件编排;AnimatePresence 会让已移除的组件继续保持挂载,直到退出动画完成;layout 和 layoutId 会在布局变化时运行 FLIP 动画;useScroll 和 useInView 会根据滚动位置驱动动效。 为 x、y、scale、rotate 和 opacity 设置动画,并在根节点设置 MotionConfig reducedMotion="user"。

导入: import { motion } from "motion/react" — 该包在 v11 时从 framer-motion 更名而来。

详细资源:


适用路径

  • 元素进入或离开 React 树 — 动画需要用 AnimatePresence 包裹元素,并为其设置 key;请参阅 examples/core.md。
  • 元素保持挂载,但尺寸或位置发生变化 — layout 会测量变化前后的状态,并为差异添加动画;请参阅 examples/layout.md。
  • 同一元素在不同时刻出现在两个位置 — 共享的 layoutId 会让元素移动到另一处;请参阅 examples/layout.md。
  • 滚动位置驱动数值变化 — 使用 useScroll 搭配 useTransform,或使用 whileInView 触发一次性动画;请参阅 examples/scroll.md。
  • React 状态之外的事件触发动画 — 例如计时器、响应或错误,此时 useAnimation 可提供命令式控制句柄;请参阅 examples/sequences.md。

<critical_requirements>

编写 Motion 代码前

移除时需要动画的内容都要用 AnimatePresence 包裹。 React 会在条件变为 false 的瞬间卸载节点,因此如果没有包装器将节点保留在树中,exit 就没有可作用的对象。

为 AnimatePresence 的每个直接子元素设置稳定且唯一的 key。 AnimatePresence 通过 key 匹配即将移除的元素;列表变化时,索引 key 会指向另一个元素,导致错误的元素执行移出动画。

对 x、y、scale、rotate 和 opacity 添加动画。 Motion 会将这些属性写入 transform 和 opacity,由合成器负责处理;而 height、width 和 marginTop 会在每一帧重新执行布局。

在应用根节点设置 MotionConfig reducedMotion="user"。 对于已在操作系统中开启减少动态效果的用户,变换和布局动画会自动禁用,而透明度和颜色仍可正常变化,因此状态变化依然清晰可辨,不会瞬间完成。

</critical_requirements>


自动检测: motion/react、framer-motion、motion.div、AnimatePresence、MotionConfig、 LayoutGroup、LazyMotion、layoutId、whileHover、whileTap、whileDrag、whileInView、useAnimation、 useScroll、useTransform、useMotionValue、useSpring、useInView、usePageInView、useReducedMotion、 useDragControls、staggerChildren、delayChildren、Variants

适用场景:

  • 与 React 挂载生命周期相关的进入、退出和存在动画
  • 通过 variants 和交错动画,由一个父元素协调多个元素
  • 手势驱动的动态效果——悬停、轻触、拖动、聚焦
  • 滚动触发和滚动联动效果
  • 容器间的布局变化和共享元素过渡
  • 由 React 状态无法建模的事件触发的命令式序列

其他方案处理:

  • 纯声明式的状态反馈——例如悬停颜色、焦点环——没有元素挂载、卸载或移动。样式层可直接处理,无需组件包装器
  • 帧级时间轴编辑,交付物是可拖动播放头的时间轴,而非一组组件状态
  • 整个文档的视图切换,由浏览器将退出页面和进入页面合成在一起

Motion 为状态而非时间添加动画。组件声明每种状态下的外观,Motion 负责计算插值、中断以及延续的速度——因此,弹簧动画即使在运行中被打断,也会从当前状态继续,而不是重新开始。


<decision_framework>

何时值得使用 Motion

满足以下任意一项时,Motion 才值得引入组件包装器和额外的包体积:

  • 元素即将从树中移除,并且必须在移除前播放动画
  • 起始值需要测量而非预先编写——例如布局变化、拖动释放或被打断的弹簧动画
  • 需要由一个触发器协调多个元素,并在退出时交错播放和反向播放
  • 动画值由连续输入推导而来,例如滚动进度

如果以上条件都不满足,那么这只是一个起始值和结束值都预先已知的状态变化,不需要运行时。

Variants 还是直接属性

当多个元素共享动画,或父元素需要控制子元素的时序时,应使用 variants;staggerChildren 和 delayChildren 仅在 variants 方式中可用,而且子元素会继承父元素的 variant 名称,无需传递任何内容。对于只有两种状态的单个元素,直接内联编写 initial/animate 更清晰。

</decision_framework>


核心模式

模式 1:Motion 组件

任何以 motion. 为前缀的 HTML 或 SVG 标签都接受 initial、animate、exit 和 transition。

import { motion } from "motion/react";

export const FadeIn = ({ children }: { children: React.ReactNode }) => (
  <motion.div
    initial={{ opacity: 0, y: 20 }}
    animate={{ opacity: 1, y: 0 }}
    transition={{ duration: 0.3 }}
  >
    {children}
  </motion.div>
);

y 会编译为变换;而 marginTop 或 top 会在每一帧重新执行布局。

完整代码:examples/core.md


模式 2:编排变体

变体为动画状态命名,让父级可以按名称驱动子元素并控制其时序。

import { motion, type Variants } from "motion/react";

const containerVariants: Variants = {
  hidden: { opacity: 0 },
  visible: { opacity: 1, transition: { staggerChildren: 0.1 } },
};

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

子元素会继承父级当前的变体名称,因此条目元素本身无需携带 animate 属性。staggerDirection: -1 会在退出时反转级联顺序,让列表从底部开始逐项收起。

完整代码:examples/core.md


模式 3:AnimatePresence

在组件被移除后,继续将其保留在树中,直到其 exit 动画结束。

import { AnimatePresence, motion } from "motion/react";

<AnimatePresence>
  {isOpen && (
    <motion.div
      key="modal"
      initial={{ opacity: 0, scale: 0.95 }}
      animate={{ opacity: 1, scale: 1 }}
      exit={{ opacity: 0, scale: 0.95 }}
    />
  )}
</AnimatePresence>

mode="sync"(默认)会让退出和进入动画重叠;mode="wait" 会等退出动画结束后再开始进入动画,适用于页面过渡;mode="popLayout" 会将退出元素移出布局流,让其余同级元素动画移动到位。

完整代码:examples/core.md


模式 4:手势

whileHover、whileTap、whileFocus 和 whileDrag 描述一种在手势持续期间保持、并在手势结束时恢复的状态。

<motion.button
  whileHover={{ scale: 1.05 }}
  whileTap={{ scale: 0.95 }}
  transition={{ type: "spring", stiffness: 400, damping: 17 }}
/>

拖动还支持 drag、dragConstraints(引用或像素范围框)、用于越过边界时增加阻力的 dragElastic,以及在由元素本身以外的对象启动拖动时使用的 useDragControls。

完整代码:examples/core.md


模式 5:布局动画

layout 会在 React 提交前后测量元素并为差异添加动画,因此 flex 顺序、网格位置或内容尺寸的变化无需编写起始值也能产生动画效果。

<motion.div layout transition={LAYOUT_SPRING}>
  <motion.h2 layout="position">Title</motion.h2>
</motion.div>

{activeTab === tab && <motion.div layoutId="indicator" />}

layout="position" 会为子元素的位置添加动画,但不改变其缩放,这样父级调整大小时文本就不会被拉伸。layoutId 会匹配两个不会同时挂载的元素,并为从一个元素到另一个元素的变化添加动画;该 ID 是全局的,因此当组件可能在页面上出现多次时,可用 LayoutGroup id={...} 为其限定作用域。

完整代码:examples/layout.md


模式 6:滚动驱动的动效

whileInView 会在进入视口时触发一次;useScroll 配合 useTransform 会将连续的滚动进度映射到一个值。

<motion.div
  initial={{ opacity: 0, y: 50 }}
  whileInView={{ opacity: 1, y: 0 }}
  viewport={{ once: true, margin: "-100px" }}
/>;

const { scrollYProgress } = useScroll({
  target: ref,
  offset: ["start end", "end start"],
});
const y = useTransform(scrollYProgress, [0, 1], [-100, 100]);

useScroll 返回的值是一个运动值,会在 React 之外更新,因此不会在每个滚动帧都触发重新渲染。

完整代码:examples/scroll.md


模式 7:弹簧与补间过渡
// Springs: physics, no fixed duration, survive interruption
const BOUNCY = { type: "spring", stiffness: 300, damping: 10 };
const SNAPPY = { type: "spring", stiffness: 500, damping: 30 };

// Tweens: fixed duration and curve
const ENTER = { type: "tween", ease: "easeOut", duration: 0.3 };
const EXIT = { type: "tween", ease: "easeIn", duration: 0.2 };

弹簧适用于任何用户可以打断的动效——按钮、卡片、拖动等——因为打断时速度会延续。补间适用于必须在确定时间内完成的动效,例如需要与其他内容协调的模态框或页面切换。

预设:reference.md


模式 8:使用 useAnimation 进行命令式控制

适用于由 React 状态无法表示的事件触发的动画,例如计时器、响应或错误。

const controls = useAnimation();

useEffect(() => {
  if (hasError) {
    controls.start({ x: [0, -10, 10, -10, 0], transition: { duration: 0.3 } });
  }
}, [hasError, controls]);

<motion.div animate={controls}>{children}</motion.div>;

向 controls.start 传入数组会依次运行各个步骤,每一步都会等待上一步完成。

完整代码:examples/sequences.md


模式 9:减少动态效果
// Whole app
<MotionConfig reducedMotion="user">{children}</MotionConfig>

// One component, where the reduced form is different rather than absent
const shouldReduceMotion = useReducedMotion();
<motion.div
  initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 50 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: shouldReduceMotion ? 0.2 : 0.5 }}
/>

reducedMotion="user" 会禁用变换和布局动画,同时保留不透明度和颜色变化,因此应用仍能显示内容发生了变化。当减少动态效果时需要使用专属值,而不是简单地移除变换时,可使用 useReducedMotion。

完整代码:examples/core.md


模式 10:交错节奏与标签页可见性

stagger()(v12+)应设置在 delayChildren 上,而不是 staggerChildren 上——它会返回一个函数来计算每个子元素的延迟,因此可以让级联动画从中心开始或沿缓动曲线运行。

import { stagger, usePageInView } from "motion/react";

const transition = {
  delayChildren: stagger(0.05, { from: "center", ease: "easeOut" }),
};
// from: "first" (default) | "center" | "last" | an index

const isPageVisible = usePageInView(); // v12.19+, true on the server

usePageInView 会报告标签页的可见性;循环动画应据此控制是否运行,以免在后台标签页中持续消耗帧。

完整代码:examples/sequences.md


<red_flags>

危险信号

运行时会出错:

  • 对上方没有 AnimatePresence 的组件使用 exit——React 会先移除节点,导致该属性无法运行——请包裹条件渲染
  • 将 React Fragment 作为 AnimatePresence 的直接子项——Fragment 不接受 key,因此无法追踪任何内容,所有退出动画都会跳过——为每个元素分别设置带 key 的条件渲染
  • 在动画列表子项上使用 key={index}——插入或排序时,键会指向其他项目,导致错误的元素执行退出动画——请使用稳定的 ID 作为键
  • 为 height、width、top、left、margin 或 padding 添加动画——每一帧都会触发布局重排——请改为对 scale 和 x/y 添加动画,并将 transformOrigin 设置为应开始扩展的位置
  • 父级使用 layout,但文本子项缺少 layout="position"——子项会随盒子缩放,文字会明显变形——请为每个子项添加该属性
  • 未处理减少动态效果的偏好——用户要求操作系统关闭动态效果,却仍会看到完整位移的动画——请在根节点设置 MotionConfig reducedMotion="user"

容易出乎意料的行为:

  • layoutId 是全局的,因此同一页面上同一组件的两个实例会争用它——请使用 LayoutGroup id={...} 限定作用域
  • mode="wait" 会串行执行退出和进入动画,因此用户感知到的延迟是两者时长之和
  • whileInView 通过 viewport 配置,而 useScroll 通过 offset 配置——这两个属性名不能互换
  • 只有 motion.path、motion.circle 及其同类元素会执行动画;motion.svg 内的普通 <path> 不会动
  • 服务端渲染期间,useInView 会返回 false,因此除非将初始状态设为可见,否则服务端标记呈现的是隐藏状态
  • Motion 值不会触发重新渲染,这是有意为之——当需要运行副作用时,请通过 useMotionValueEvent 读取它们
  • 在同一个元素上同时使用 drag 和 layout 会争用同一个变换;请在拖动期间禁用布局动画
  • 刚度高、阻尼低的弹簧会过度回弹,甚至裁切实际内容,而短占位字符串无法暴露这个问题
  • 手动设置的 willChange 会与 Motion 自身的图层管理冲突;Motion 已会将正在动画的内容提升为独立图层

评论

支持 Markdown