关于 motion-design-patterns
面向 React 项目的 Motion 动画设计模式技能,提供弹簧、列表交错、布局、滚动触发和页面过渡等实现参考。适合构建或润色界面的开发者,强调动画目的性、性能与全项目的一致性。
主要功能
- 01弹簧动画参数参考
- 02淡入上移与列表交错
- 03布局动画和共享布局
- 04悬停与按压交互
- 05滚动触发与页面过渡
适用场景
- 01为卡片网格添加入场和悬停效果
- 02实现标签切换或手风琴展开动画
- 03为落地页添加滚动触发效果
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dylanfeltus/ skills --skill motion-design-patterns安装后在 Claude Code 中输入/motion-design-patterns确认已加载。
附带文件README.md
详细介绍
动效设计模式
适用于 React 的 Framer Motion(Motion)模式——弹簧动画、交错动画、布局动画、微交互、滚动触发效果和退出动画。这是区分普通 UI 与精致 UI 的首要因素。
适用场景
- 在 React 项目中构建或改进 UI 动画
- 用户提出“精致感”“惊喜感”“微交互”或“让它用起来更舒服”等需求
- 添加入场/退出动画、悬停效果或页面过渡
- 让列表、卡片、模态框或导航更显高级
- 用户提到 Magic UI、Motion Primitives 或 Framer Motion
核心理念
- 动效应有明确目的。 每个动画都应传达某种信息——状态变化、层级、空间关系或反馈。
- 少即是多。 一个调校得当的弹簧动画胜过五个相互冲突的动画。
- 性能优先。 只为
transform和opacity添加动画。切勿为width、height、top、left或margin添加动画。 - 保持一致很重要。 在整个项目中使用相同的弹簧配置。
依赖项
npm install motion
导入:import { motion, AnimatePresence, stagger, useScroll, useTransform } from "motion/react"
注意:该软件包于 2024 年末从 framer-motion 更名为 motion。两者都可用,但目前的软件包名称是 motion。
弹簧配置
弹簧动画比缓动曲线更自然。可将以下配置用作默认值:
推荐默认值
// Snappy — buttons, toggles, small elements
const snappy = { type: "spring", stiffness: 500, damping: 30 }
// Smooth — cards, panels, modals
const smooth = { type: "spring", stiffness: 300, damping: 25 }
// Gentle — page transitions, large elements
const gentle = { type: "spring", stiffness: 200, damping: 20 }
// Bouncy — playful UI, notifications, badges
const bouncy = { type: "spring", stiffness: 400, damping: 15 }
快速参考
| 感觉 | stiffness | damping | 适用场景 |
|---|---|---|---|
| 干脆 | 500 | 30 | 按钮、切换开关、标签 |
| 平滑 | 300 | 25 | 卡片、面板、模态框 |
| 柔和 | 200 | 20 | 页面过渡、首屏区块 |
| 弹跳 | 400 | 15 | 通知、徽章、有趣的 UI |
经验法则
- stiffness 越高 = 动画越快
- damping 越低 = 弹跳越明显
- damping ratio < 1 = 会过冲(弹跳)
- 如需无弹跳,将 damping 设为 ≥ 2 × √stiffness
模式 1:淡入 + 上移
最常用的入场动画。元素淡入的同时略微向上滑动。
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
>
Content here
</motion.div>
适用场景: 卡片、区块,以及页面上出现的任何内容。
反模式: 不要使用 y: 100 或较大的值——微小的位移(12–24px)更显高级,较大的位移则会显得生硬。
模式 2:列表交错动画
子元素依次进入。级联效果能让列表显得鲜活。
const container = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.08,
delayChildren: 0.1,
},
},
}
const item = {
hidden: { opacity: 0, y: 16 },
visible: {
opacity: 1,
y: 0,
transition: { type: "spring", stiffness: 300, damping: 25 },
},
}
<motion.ul variants={container} initial="hidden" animate="visible">
{items.map((i) => (
<motion.li key={i.id} variants={item}>
{i.content}
</motion.li>
))}
</motion.ul>
时间间隔指南:
- 3–5 个项目:
staggerChildren: 0.1 - 6–12 个项目:
staggerChildren: 0.06 - 12 个以上项目:
staggerChildren: 0.03(或将它们作为一个整体添加动画)
反模式: 不要让超过约 15 个项目逐个交错出现——这样会显得缓慢。可将它们分组,或使用波浪效果。
模式 3:布局动画
在布局状态之间自动添加动画。这是 Motion 的杀手级特性。
<motion.div layout transition={{ type: "spring", stiffness: 300, damping: 25 }}>
{isExpanded ? <ExpandedContent /> : <CollapsedContent />}
</motion.div>
共享布局(标签页、活动指示器)
{tabs.map((tab) => (
<button key={tab.id} onClick={() => setActive(tab.id)}>
{tab.label}
{active === tab.id && (
<motion.div
layoutId="activeTab"
className="absolute inset-0 bg-primary rounded-md"
transition={{ type: "spring", stiffness: 400, damping: 30 }}
/>
)}
</button>
))}
适用场景: 标签页指示器、展开卡片、重新排序的列表、筛选网格。
性能提示: 如果只需要为位置(而非尺寸)添加动画,请添加 layout="position"。这样开销更低。
模式 4:退出动画(AnimatePresence)
元素从 DOM 中移除前先播放退出动画。
<AnimatePresence mode="wait">
{isVisible && (
<motion.div
key="modal"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
>
Modal content
</motion.div>
)}
</AnimatePresence>
AnimatePresence 模式
| 模式 | 行为 | 适用场景 |
|---|---|---|
"sync"(默认) | 进入和退出同时发生 | 交叉淡化效果 |
"wait" | 等退出动画结束后再进入 | 页面过渡、模态框 |
"popLayout" | 退出的元素脱离布局流 | 删除项目时的列表 |
模式 5:悬停和轻触微交互
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
>
Click me
</motion.button>
不同元素类型的悬停模式
| 元素 | whileHover | whileTap |
|---|---|---|
| 按钮(主要) | { scale: 1.02 } | { scale: 0.98 } |
| 卡片 | { y: -4, boxShadow: "0 8px 30px rgba(0,0,0,0.12)" } | — |
| 图标按钮 | { scale: 1.1 } | { scale: 0.9 } |
| 链接 | { x: 2 } | — |
| 头像 | { scale: 1.05 } | — |
反模式: 按钮的缩放比例不要超过 1.05——这会显得过于卡通。微小的缩放(1.01–1.03)更显高级。
模式 6:滚动触发动画
滚动进入视口时播放动画
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ type: "spring", stiffness: 200, damping: 20 }}
>
Appears when scrolled into view
</motion.div>
viewport.once: true — 仅在首次进入视口时播放动画(最常用于落地页)。
viewport.margin — 负边距会提前触发动画(在元素完全进入视口之前)。
与滚动关联的进度
const { scrollYProgress } = useScroll()
const opacity = useTransform(scrollYProgress, [0, 0.3], [1, 0])
const y = useTransform(scrollYProgress, [0, 0.3], [0, -50])
<motion.div style={{ opacity, y }}>
Parallax hero content
</motion.div>
模式 7:页面过渡
// In your layout or page wrapper
<AnimatePresence mode="wait">
<motion.main
key={pathname}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
>
{children}
</motion.main>
</AnimatePresence>
保持微妙。 页面过渡应该快速(时长 200–300ms),移动幅度也要小(8–12px)。花哨的页面过渡会让人感觉像是 2015 年的产物。
模式 8:数字/文本过渡
// Animate a counter
<motion.span
key={count}
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
>
{count}
</motion.span>
将内容包裹在 AnimatePresence 中,以实现退出动画。非常适合仪表盘、定价和实时数据。
应避免的反模式
| ❌ 不要 | ✅ 应该 |
|---|---|
直接为 width/height 添加动画 | 使用 scale 或 layout 动画 |
使用较大的移动值(y: 200) | 使用微妙的值(y: 16–24) |
| 所有元素都添加弹跳效果 | 仅在活泼或庆祝的场景中使用弹跳效果 |
| 每次滚动事件都触发动画 | 使用带有 once: true 的 whileInView |
| 每个元素都使用不同的时间配置 | 在整个项目中使用一致的弹簧配置 |
| 页面加载时为所有内容添加动画 | 优先处理首屏内容;其余内容使用错开动画 |
| 自定义缓动曲线 | 使用弹簧——它们更能适应中断 |
推荐的组件库参考
构建带动画的组件时,可参考以下库中的模式和灵感:
- Magic UI — 150 多个带动画的 React 组件,兼容 shadcn/ui
- Motion Primitives — 可复制粘贴的 React 动效组件
- Aceternity UI — 时髦的带动画组件(更重、更夸张)
当用户需要某个特定的带动画组件(文本显现、动画边框、渐变动画等)时,先查看这些组件库——其中很可能有经过实践检验的实现方案。
快速决策指南
| 场景 | 模式 | 弹簧配置 |
|---|---|---|
| 卡片出现 | 淡入 + 上移 | smooth |
| 列表加载 | 错开列表 | smooth,错开 0.08s |
| 切换标签页 | 共享布局(layoutId) | snappy |
| 打开/关闭模态框 | AnimatePresence + 缩放 | smooth |
| 按钮按下 | whileHover + whileTap | snappy |
| 落地页版块 | 滚动触发 | gentle |
| 页面导航 | 页面过渡 | smooth |
| 仪表盘计数器 | 数字过渡 | snappy |
| 通知弹窗 | 淡入 + 上移 + 弹跳 | bouncy |
| 手风琴展开 | 布局动画 | smooth |
示例
示例 1:“为这个卡片网格添加动画”
为网格应用错开的淡入上移动画,并为每张卡片添加悬停上移效果:
<motion.div
variants={container}
initial="hidden"
animate="visible"
className="grid grid-cols-3 gap-4"
>
{cards.map((card) => (
<motion.div
key={card.id}
variants={item}
whileHover={{ y: -4, boxShadow: "0 8px 30px rgba(0,0,0,0.12)" }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
>
<Card {...card} />
</motion.div>
))}
</motion.div>
示例 2:“让这个模态框的效果更好”
使用 AnimatePresence 包裹,添加缩放和不透明度的进入/退出动画,并让遮罩淡入:
<AnimatePresence>
{isOpen && (
<>
<motion.div
className="fixed inset-0 bg-black/50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
<motion.div
className="fixed inset-0 flex items-center justify-center"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
>
<ModalContent onClose={onClose} />
</motion.div>
</>
)}
</AnimatePresence>
示例 3:“为这个落地页添加滚动动画”
为每个版块应用 whileInView,并让子元素错开播放动画:
<motion.section
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ type: "spring", stiffness: 200, damping: 20 }}
>
<h2>Feature Section</h2>
<motion.div
variants={container}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{features.map((f) => (
<motion.div key={f.id} variants={item}>
<FeatureCard {...f} />
</motion.div>
))}
</motion.div>
</motion.section>