关于 framer-motion
面向 Claude Code、claude.ai 和 Cowork 的动效与交互技能,涵盖 Motion/Framer Motion 用法,并建议沿用项目现有包名,避免未经请求迁移。
主要功能
- 01Motion 与 Framer Motion API 指引
- 02退出与布局动画模式
- 03Variants 与手势指引
- 04随版本变化的 API 说明
适用场景
- 01为条件显示的界面添加退出动画
- 02为共享或变化的布局添加动画
- 03确认项目当前使用的包名
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ athevon/ genjutsu --skill framer-motion安装后在 Claude Code 中输入/framer-motion确认已加载。
详细介绍
与版本相关。 以下每个 API 名称、SDK 要求和浏览器支持声明都已于 2026-09-08 根据一手资料核实。核对了哪些资料以及核对时间,见
_jutsu/VERSIONS.md。如果该日期已过,应在使用版本号前重新核实。
Framer Motion - 子技能
两个包名,同一个库。 Framer Motion 已更名为 Motion。
motion和framer-motion发布的版本相同(截至 2026-09-08 为 13.2.0):motion声明"framer-motion": "^13.2.0"为依赖项,且motion/react会重新导出它。两者都没有问题,而且 大多数已安装的项目仍使用framer-motion。读取
package.json并遵循其中的配置。 除非用户提出要求,否则不要将项目从一个包名迁移到另一个——这是cast中的铁律 8 /paint中的铁律 10。
package.json中的内容导入路径 安装命令 motion"motion/react"npm install motionframer-motion"framer-motion"npm install framer-motion两者都有 使用你正在编辑的文件已经导入的那个;说明项目正处于迁移过程中 - 本技能中的所有 API 在 v13 下的两个包名中完全相同。符号若是 v11 之后新增的,会在行内标注。对等依赖:
react/react-dom^18 || ^19。
何时使用 Framer Motion,而非其他方案
| 标准 | Framer Motion | GSAP | 原生 CSS |
|---|---|---|---|
| 布局动画 | 出色(layoutId) | 手动实现 | 不支持 |
| 退出动画 | AnimatePresence | 时间轴反向播放 | 有限(display) |
| 手势(拖动、悬停) | 原生、声明式 | Draggable 插件 | 基础支持 |
| 滚动驱动 | useScroll + useTransform | ScrollTrigger(功能更强) | scroll-timeline |
| 复杂编排 | Variants + 传播 | 时间轴(更灵活) | @keyframes |
| 包体积 | 约 50kb(tree-shaking 后) | 核心约 30kb | 0kb |
| React 集成 | 原生、组件优先 | Refs + useGSAP | className 切换 |
规则:React UI 交互(模态框、提示、重排、共享布局)使用 Framer Motion。复杂时间轴、电影感滚动驱动动画、SVG 变形使用 GSAP。
AnimatePresence - 退出动画
<AnimatePresence mode="wait">
{isVisible && (
<motion.div
key="unique-key" // REQUIRED - identifies the component
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>
mode="wait"- 等待退出动画完成后再进入(页面过渡)mode="sync"- 退出和进入同时进行mode="popLayout"- 立即从文档流中移除(适用于列表)onExitComplete- 所有退出动画完成时调用的回调
布局动画
// Shared layout - the element "slides" between two positions
<motion.div layoutId="highlight" className={activeTab === id ? "active" : ""} />
// Auto layout - animates position/size when layout changes
<motion.div layout>
{isExpanded && <motion.p layout>Additional content</motion.p>}
</motion.div>
// layout="position" - animates position only (not size)
// layout="size" - animates size only
// layout="preserve-aspect" - preserves the ratio during the transition
Variants - 传播与编排
import { stagger } from "motion/react";
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
// staggerChildren + staggerDirection are DEPRECATED (Motion 12.22, Jul 2025).
// Pass a stagger() function to delayChildren instead.
delayChildren: stagger(0.08, { startDelay: 0.2 }),
// reverse order: stagger(0.08, { from: "last" })
// also available: from: "first" | "center" | "last" | index, and ease
},
},
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 },
};
<motion.ul variants={container} initial="hidden" animate="show">
{items.map((i) => (
<motion.li key={i.id} variants={item} />
))}
</motion.ul>
Variants 会自动传播到 motion 子组件 - 无需在子组件上设置 initial/animate。
手势
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
whileFocus={{ borderColor: "#3b82f6" }}
// Drag
drag // true = x+y, "x" = horizontal only, "y" = vertical only
dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
dragElastic={0.2} // 0 = rigid, 1 = free (default 0.35)
dragSnapToOrigin // returns to initial position
onDragEnd={(e, info) => {
if (info.offset.x > 100) handleSwipe("right");
}}
/>
Motion values - 无需重新渲染的响应式更新
const x = useMotionValue(0);
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0]);
const background = useTransform(x, [-200, 200], ["#ff0000", "#00ff00"]);
// Spring-based smoothing
const smoothX = useSpring(x, { stiffness: 300, damping: 30 });
// Scroll tracking
const { scrollY, scrollYProgress } = useScroll();
const parallaxY = useTransform(scrollYProgress, [0, 1], [0, -300]);
// Element-scoped scroll
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"],
});
Motion values 不会触发 React 重新渲染 - 它们会通过 style 直接更新 DOM。
不要这样做
使用 styled-components / Emotion 时,不要在未设置 isValidProp 的情况下使用 motion(v13+)
Motion 13.0 移除了 @emotion/is-prop-valid 这一可选依赖。若未显式注入,motion 专用属性会泄漏到 DOM 上。
import isPropValid from "@emotion/is-prop-valid";
import { MotionConfig } from "motion/react";
<MotionConfig isValidProp={isPropValid}>
<App />
</MotionConfig>
也可以反过来组合,让样式库负责转发属性:const MotionDiv = motion.create(StyledDiv)。
不要在回调中不加保护地调用 setState
// BAD - infinite re-render if animate depends on state
onUpdate={(latest) => setPosition(latest.x)}
// GOOD - guard or useMotionValueEvent
const x = useMotionValue(0);
useMotionValueEvent(x, "change", (latest) => {
if (latest > threshold) onThresholdReached();
});
没有稳定的 key 时,不要使用布局动画
// BAD - key changes every render, breaks layout tracking
<motion.div layout key={Math.random()} />
// GOOD - stable key derived from data
<motion.div layout key={item.id} />
不要忘记为 AnimatePresence 设置唯一的 key
// BAD - no key, exit animation does not trigger
<AnimatePresence>
{isOpen && <motion.div exit={{ opacity: 0 }} />}
</AnimatePresence>
// GOOD - unique key for each conditional child
<AnimatePresence>
{isOpen && <motion.div key="modal" exit={{ opacity: 0 }} />}
</AnimatePresence>
不要用 motion.div 包裹已经带动画效果的组件
// BAD - double animation, transform conflicts
<motion.div animate={{ x: 100 }}>
<motion.div animate={{ x: -50 }}>Content</motion.div>
</motion.div>
// GOOD - single animation level per transform axis
<motion.div animate={{ x: 100 }}>
<motion.div animate={{ opacity: 0.5 }}>Content</motion.div>
</motion.div>
// GOOD - use variants to coordinate parent/child
<motion.div variants={parent} animate="active">
<motion.div variants={child} />
</motion.div>