关于 web-animation-framer-motion
面向 React 开发者的 Motion 动画模式指南,介绍如何为组件状态、布局变化和滚动添加动画。内容涵盖变体编排、进出场、手势及减少动态效果的支持。
主要功能
- 01使用 motion 组件定义动画状态
- 02用变体编排多个子元素
- 03为组件离场运行动画
- 04动画化布局变化与共享元素
- 05基于滚动和手势驱动动画
- 06支持减少动态效果偏好
适用场景
- 01为条件渲染的组件添加进出场效果
- 02编排列表项目的交错动画
- 03为标签或卡片布局变化添加动画
- 04制作滚动触发或手势驱动效果
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-animation-framer-motion安装后在 Claude Code 中输入/web-animation-framer-motion确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
metadata.yamlreference.mdexamples/core.mdlayout.mdscroll.mdsequences.mdsvg.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更名而来。
详细资源:
- examples/core.md — 动效组件、variants、AnimatePresence、手势、减少动效
- examples/layout.md —
layout、layoutId、可展开卡片、标签页指示器 - examples/scroll.md — 滚动进度、进入视口时显示、视差
- examples/sequences.md —
useAnimation链、关键帧数组、stagger()塑形 - examples/svg.md —
pathLength绘制效果 - reference.md — v11/v12 迁移、过渡预设、属性和 Hook 表格
适用路径
- 元素进入或离开 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 传入数组会依次运行各个步骤,每一步都会等待上一步完成。
模式 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 会报告标签页的可见性;循环动画应据此控制是否运行,以免在后台标签页中持续消耗帧。
<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 已会将正在动画的内容提升为独立图层