motion-design-patterns by dylanfeltus/skills
npx skills add https://github.com/dylanfeltus/skills --skill motion-design-patterns适用于 React 的 Framer Motion 模式——弹簧动画、交错效果、布局动画、微交互、滚动触发效果以及退出动画。这是区分普通 UI 与精致 UI 的首要因素。
transform 和 opacity 进行动画处理。切勿对 width、height、top、left 或 margin 进行动画处理。广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
npm install motion
导入:import { motion, AnimatePresence, stagger, useScroll, useTransform } from "motion/react"
注意:该包在 2024 年底从 framer-motion 更名为 motion。两者都有效,但 motion 是当前的包名。
弹簧动画比缓动曲线感觉更自然。使用以下配置作为默认值:
// 干脆利落 —— 按钮、开关、小元素
const snappy = { type: "spring", stiffness: 500, damping: 30 }
// 平滑 —— 卡片、面板、模态框
const smooth = { type: "spring", stiffness: 300, damping: 25 }
// 柔和 —— 页面过渡、大元素
const gentle = { type: "spring", stiffness: 200, damping: 20 }
// 弹性 —— 趣味性 UI、通知、徽章
const bouncy = { type: "spring", stiffness: 400, damping: 15 }
| 感觉 | 刚度 | 阻尼 | 适用场景 |
|---|---|---|---|
| 干脆利落 | 500 | 30 | 按钮、开关、标签 |
| 平滑 | 300 | 25 | 卡片、面板、模态框 |
| 柔和 | 200 | 20 | 页面过渡、主视觉区域 |
| 弹性 | 400 | 15 | 通知、徽章、趣味性 UI |
最基础的入场动画。元素淡入的同时轻微向上滑动。
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
>
内容放在这里
</motion.div>
使用时机: 卡片、区块、页面上出现的任何内容。
反模式: 不要使用 y: 100 或较大的值——细微的移动(12–24px)感觉高级,大幅移动感觉廉价。
子元素依次动画入场。这种级联效果让列表感觉生动。
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>
时序指南:
staggerChildren: 0.1staggerChildren: 0.06staggerChildren: 0.03(或作为一组进行动画)反模式: 不要对超过约 15 个项目进行单独交错——这会感觉很慢。将它们分组或使用波浪效果。
在不同布局状态之间自动进行动画过渡。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"。这样性能开销更小。
元素在从 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 }}
>
模态框内容
</motion.div>
)}
</AnimatePresence>
| 模式 | 行为 | 适用场景 |
|---|---|---|
"sync" (默认) | 同时进入和退出 | 交叉淡入淡出效果 |
"wait" | 等待退出完成后再进入 | 页面过渡、模态框 |
"popLayout" | 退出元素从布局流中弹出 | 移除项目的列表 |
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
>
点击我
</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)感觉高级。
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ type: "spring", stiffness: 200, damping: 20 }}
>
滚动进入视口时出现
</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 }}>
视差主视觉内容
</motion.div>
// 在你的布局或页面包装器中
<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–300 毫秒)且移动幅度小(8–12px)。花哨的页面过渡感觉像 2015 年的风格。
// 动画计数器
<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 |
| 每个元素使用不同的时序 | 在整个项目范围内使用一致的弹簧配置 |
| 页面加载时为所有东西添加动画 | 优先处理首屏内容;其余的交错加载 |
| 自定义缓动曲线 | 使用弹簧动画——它们对中断的响应更好 |
构建动画组件时,可以参考以下库获取模式和灵感:
当用户想要特定的动画组件(文字显示、动画边框、渐变动画等)时,首先检查这些库——很可能已经有经过实战检验的实现。
| 场景 | 模式 | 弹簧配置 |
|---|---|---|
| 卡片出现 | 淡入 + 上升 | smooth |
| 列表加载 | 交错列表 | smooth, 0.08s stagger |
| 标签切换 | 共享布局 (layoutId) | snappy |
| 模态框打开/关闭 | AnimatePresence + scale | smooth |
| 按钮按下 | whileHover + whileTap | snappy |
| 落地页区块 | 滚动触发 | gentle |
| 页面导航 | 页面过渡 | smooth |
| 仪表板计数器 | 数字过渡 | snappy |
| 通知弹出 | 淡入 + 上升 + 弹性 | bouncy |
| 手风琴展开 | 布局动画 | smooth |
对网格应用交错的淡入+上升入场效果,对每个卡片应用悬停上浮效果:
<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>
用 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>
对每个区块应用带交错子元素的 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>功能区块</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>
每周安装量
87
代码仓库
GitHub 星标数
173
首次出现
2026年2月19日
安全审计
安装于
gemini-cli79
codex79
opencode79
github-copilot74
amp71
claude-code71
Framer Motion (Motion) patterns for React — springs, staggers, layout animations, micro-interactions, scroll-triggered effects, and exit animations. The #1 differentiator between generic and polished UI.
transform and opacity only. Never animate width, height, top, left, or margin.npm install motion
Import: import { motion, AnimatePresence, stagger, useScroll, useTransform } from "motion/react"
Note: The package was renamed from framer-motion to motion in late 2024. Both work, but motion is the current package.
Springs feel more natural than easing curves. Use these as your defaults:
// 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 }
| Feel | stiffness | damping | Use for |
|---|---|---|---|
| Snappy | 500 | 30 | Buttons, toggles, chips |
| Smooth | 300 | 25 | Cards, panels, modals |
| Gentle | 200 | 20 | Page transitions, heroes |
| Bouncy | 400 | 15 | Notifications, badges, fun UI |
The bread-and-butter entrance animation. Element fades in while sliding up slightly.
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 300, damping: 25 }}
>
Content here
</motion.div>
When to use: Cards, sections, any content appearing on the page.
Anti-pattern: Don't use y: 100 or large values — subtle (12–24px) feels premium, large feels janky.
Children animate in one after another. The cascade effect that makes lists feel alive.
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>
Timing guide:
staggerChildren: 0.1staggerChildren: 0.06staggerChildren: 0.03 (or animate as a group)Anti-pattern: Don't stagger more than ~15 items individually — it feels slow. Group them or use a wave effect.
Automatically animate between layout states. Motion's killer feature.
<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>
))}
When to use: Tab indicators, expanding cards, reordering lists, filtering grids.
Performance tip: Add layout="position" if you only need to animate position (not size). It's cheaper.
Elements animate out before being removed from the 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>
| Mode | Behavior | Use for |
|---|---|---|
"sync" (default) | Enter and exit at the same time | Crossfade effects |
"wait" | Wait for exit to finish before entering | Page transitions, modals |
"popLayout" | Exiting element pops out of layout flow | Lists where items are removed |
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
>
Click me
</motion.button>
| Element | whileHover | whileTap |
|---|---|---|
| Button (primary) | { scale: 1.02 } | { scale: 0.98 } |
| Card | { y: -4, boxShadow: "0 8px 30px rgba(0,0,0,0.12)" } | — |
| Icon button | { scale: 1.1 } | { scale: 0.9 } |
| Link | { x: 2 } |
Anti-pattern: Don't scale buttons more than 1.05 — it looks cartoonish. Subtle (1.01–1.03) feels premium.
<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 — Only animate the first time (most common for landing pages). viewport.margin — Negative margin triggers earlier (before element is fully visible).
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>
// 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>
Keep it subtle. Page transitions should be fast (200–300ms feel) and small (8–12px movement). Flashy page transitions feel like 2015.
// 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>
Wrap in AnimatePresence for the exit animation. Great for dashboards, pricing, live data.
| ❌ Don't | ✅ Do Instead |
|---|---|
Animate width/height directly | Use scale or layout animations |
Large movement values (y: 200) | Subtle values (y: 16–24) |
| Bounce on everything | Reserve bounce for playful/celebratory moments |
| Animate on every scroll event | Use whileInView with once: true |
When building animated components, reference these for patterns and inspiration:
When a user wants a specific animated component (text reveal, animated border, gradient animation, etc.), check these libraries first — there's likely a battle-tested implementation.
| Scenario | Pattern | Spring Config |
|---|---|---|
| Card appearing | Fade + Rise | smooth |
| List loading | Staggered List | smooth, 0.08s stagger |
| Tab switching | Shared Layout (layoutId) | snappy |
| Modal open/close | AnimatePresence + scale | smooth |
| Button press | whileHover + whileTap | snappy |
| Landing page sections | Scroll-triggered | gentle |
| Page navigation | Page Transition | smooth |
| Dashboard counter | Number Transition | snappy |
| Notification popup |
Apply staggered fade+rise entrance to the grid, hover lift effect on each card:
<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>
Wrap in AnimatePresence, add scale + opacity entrance/exit, overlay fade:
<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>
Apply whileInView with staggered children to each section:
<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>
Weekly Installs
87
Repository
GitHub Stars
173
First Seen
Feb 19, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
gemini-cli79
codex79
opencode79
github-copilot74
amp71
claude-code71
TanStack Table 中文指南:React 无头表格库,实现排序过滤分页
541 周安装
| — |
| Avatar | { scale: 1.05 } | — |
| Different timing for every element | Use consistent spring configs project-wide |
| Animation on page load for everything | Prioritize above-the-fold; stagger the rest |
| Custom easing curves | Use springs — they respond to interruption better |
| Fade + Rise + bounce |
| bouncy |
| Accordion expand | Layout animation | smooth |