mcpField
F
framer-motion

framer-motion

github.com/athevon设计与 UI705989 次安装3 周前更新
代码仓库

关于 framer-motion

面向 Claude Code、claude.ai 和 Cowork 的动效与交互技能,涵盖 Motion/Framer Motion 用法,并建议沿用项目现有包名,避免未经请求迁移。

主要功能

  1. 01Motion 与 Framer Motion API 指引
  2. 02退出与布局动画模式
  3. 03Variants 与手势指引
  4. 04随版本变化的 API 说明

适用场景

  1. 01为条件显示的界面添加退出动画
  2. 02为共享或变化的布局添加动画
  3. 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 motion
framer-motion"framer-motion"npm install framer-motion
两者都有使用你正在编辑的文件已经导入的那个;说明项目正处于迁移过程中-

本技能中的所有 API 在 v13 下的两个包名中完全相同。符号若是 v11 之后新增的,会在行内标注。对等依赖:react / react-dom ^18 || ^19。

何时使用 Framer Motion,而非其他方案

标准Framer MotionGSAP原生 CSS
布局动画出色(layoutId)手动实现不支持
退出动画AnimatePresence时间轴反向播放有限(display)
手势(拖动、悬停)原生、声明式Draggable 插件基础支持
滚动驱动useScroll + useTransformScrollTrigger(功能更强)scroll-timeline
复杂编排Variants + 传播时间轴(更灵活)@keyframes
包体积约 50kb(tree-shaking 后)核心约 30kb0kb
React 集成原生、组件优先Refs + useGSAPclassName 切换

规则: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>

评论

支持 Markdown