关于 ui-animation
用于设计 UI 动画的工作流技能,提供时长、Motion 与 CSS/Tailwind 技术、无障碍和性能方面的指导,面向实现界面过渡、手势和布局变化的开发者。
主要功能
- 01按动画类型选择技术
- 02提供缓动与时长建议
- 03涵盖 Motion 手势与布局过渡
- 04考虑减少动态效果偏好
- 05检查性能与焦点行为
- 06提供可复用动画示例
适用场景
- 01为模态框和下拉菜单添加进出场效果
- 02为手势控件和布局变化添加动画
- 03为偏好减少动态效果的用户调整动画
- 04检查动画性能与焦点行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ lukasstrickler/ ai-dev-atelier --skill ui-animation安装后在 Claude Code 中输入/ui-animation确认已加载。
附带文件references/recipes.md
详细介绍
UI 动画
恰到好处的 UI 动画,兼顾时机、无障碍性和性能。
快速入门
技术选择:
- 简单过渡? → CSS/Tailwind(
transition-*、animate-*) - 进入/退出时卸载? → Motion +
AnimatePresence - 由手势驱动? → Motion 弹簧动画
- 布局变化? → Motion
layout属性
默认流程: 从缓动决策树 → 时长指南 → 实现 → 添加无障碍支持 → 验证
核心原则
- 自然运动:模拟物理规律。避免线性缓动——没有任何东西会以恒定速度移动。
- 有明确目的:每个动画都必须传达意义。如果无法说明它的好处,就删掉它。
- 快速:UI 动画控制在 300ms 以内。悬停效果控制在 150ms 以内。超过 500ms 会显得迟缓。
- 可中断:由手势驱动的动画使用弹簧动画——它们能妥善处理动画中断。
- 无障碍:始终遵循
prefers-reduced-motion。这点不可妥协。
工作流
步骤 1:对动画类型进行分类
| 类型 | 示例 | 技术 |
|---|---|---|
| 微交互 | 按钮按下、切换、复选框 | CSS/Tailwind |
| 进入/退出 | 模态框、提示消息、下拉菜单 | Motion + AnimatePresence |
| 布局变化 | 手风琴、重新排序、展开 | Motion layout 属性 |
| 共享元素 | 标签指示器、卡片展开 | Motion layoutId |
| 手势 | 拖动、滑动、下拉刷新 | Motion 弹簧动画 |
步骤 2:选择时机
- 使用下方的缓动决策树选择曲线
- 使用时长指南选择时长
- 对于手势,使用弹簧动画配置
步骤 3:实现
- 在
references/recipes.md中查看可直接复制粘贴的模式 - 应用步骤 2 中选择的时机
- 用
AnimatePresence包裹卸载时需要播放动画的元素
从设计截图或模型图开始时,使用 ui_to_artifact。使用 ui_diff_check 对比预期 UI 与实现结果。
步骤 4:无障碍(必需)
- 使用
useReducedMotion()或motion-safe:检查prefers-reduced-motion - 对减少动态效果的用户,将动画简化为仅改变不透明度
- 验证焦点时机(动画开始后再移动焦点)
步骤 5:验证
- 退出动画会播放(而非立即卸载)
-
opacity: 0的元素设置了pointerEvents: none - 动画开始后再移动焦点,而非动画开始前
- 仅对
transform和opacity设置动画
缓动
决策树
What triggers the animation?
│
├─ User action (click, tap, open)?
│ └─ Use: ease-out (fast start, slow end = responsive)
│
├─ Element moving on-screen (tab switch, reorder)?
│ └─ Use: ease-in-out (accelerate then decelerate)
│
├─ Continuous/looping (spinner, marquee)?
│ └─ Use: linear (constant speed appropriate here)
│
├─ Gesture-based (drag, swipe, pull)?
│ └─ Use: Spring animation (physics-based, interruptible)
│
└─ Hover/focus effect?
└─ Use: CSS ease, 150ms (subtle, immediate)
快速参考
| 用途 | CSS | Tailwind | 时长 |
|---|---|---|---|
| 模态框/抽屉进入 | cubic-bezier(0.32, 0.72, 0, 1) | ease-out duration-200 | 200ms |
| 模态框/抽屉退出 | cubic-bezier(0.32, 0.72, 0, 1) | ease-out duration-150 | 150ms |
| 屏幕内移动 | cubic-bezier(0.4, 0, 0.2, 1) | ease-in-out duration-200 | 200-300ms |
| 悬停效果 | ease | ease duration-150 | 150ms |
| 按钮按下 | — | active:scale-[0.97] | 即时 |
专业曲线
| 名称 | 值 | 使用场景 |
|---|---|---|
| Vaul(顺滑) | cubic-bezier(0.32, 0.72, 0, 1) | 抽屉、侧滑面板、模态框 |
| 强调 | cubic-bezier(0.2, 0, 0, 1) | Material Design 3 |
| 灵敏 | cubic-bezier(0.25, 1, 0.5, 1) | 快速 UI 过渡 |
避免使用:内置的 ease-in——起步缓慢,感觉迟钝。
时长指南
| 类型 | 时长 | 说明 |
|---|---|---|
| 微反馈 | 100-150ms | 按钮按下、切换、复选框 |
| 小型过渡 | 150-250ms | 工具提示、图标形变 |
| 中型过渡 | 200-300ms | 模态框、弹出框、下拉菜单 |
| 大型过渡 | 300-400ms | 页面过渡、复杂布局 |
| 最大值 | <500ms | 例外情况:引导流程、数据可视化 |
关键规则:
- 退出速度快于进入:进入 200ms → 退出 150ms
- 悬停要快:少于 150ms
- 高频操作要即时响应:键盘导航、滚动——少于 100ms 或不加动画
弹簧动画
基于时长(推荐)
更易与其他定时动画组合使用。使用 visualDuration(视觉上到达目标所需的时间)和 bounce(0 表示无弹跳,1 表示弹跳明显)。
| 感觉 | 配置 | 使用场景 |
|---|---|---|
| 干脆 | { duration: 0.3, bounce: 0.15 } | 选项卡、按钮、快速反馈 |
| 标准 | { duration: 0.4, bounce: 0.2 } | 模态框、菜单、常规 UI |
| 柔和 | { duration: 0.5, bounce: 0.25 } | 流畅、自然的动效 |
基于物理参数(旧版/高级)
在与物理库集成或需要精确控制弹簧动力学时使用。
| 感觉 | 配置 | 使用场景 |
|---|---|---|
| 干脆 | { stiffness: 400, damping: 30 } | 高频交互 |
| 标准 | { stiffness: 300, damping: 20 } | Framer Handshake 惯例 |
| 柔和 | { stiffness: 120, damping: 14 } | react-motion 预设 |
注意:
stiffness/damping/mass会覆盖duration/bounce。选择一种方案,不要混用。
布局动画
layout 属性
添加 layout,即可自动为位置/尺寸变化添加动画。对文本使用 layout="position"(防止变形)。
| 属性值 | 效果 | 使用场景 |
|---|---|---|
layout={true} | 为位置和尺寸变化添加动画 | 灵活元素的默认设置 |
layout="position" | 仅为位移添加动画 | 不应拉伸的文本/图标 |
layout="size" | 仅为尺寸变化添加动画 | 固定位置的展开面板 |
共享元素过渡(layoutId)
具有匹配 layoutId 的元素在进入/退出时会彼此间过渡。
关键陷阱:重复的 layoutId 值会导致元素瞬间跳到页面另一处。请在不同上下文中使用唯一 ID,或将元素放入 <LayoutGroup id="..."> 中。
布局注意事项
- 文本变形:对文本元素应用
layout="position" - 边框圆角:缩放时可能会扭曲——Motion 会自动修正,但请进行测试
- SVG 元素:
layout对<path>无效——请使用手动变形
手势注意事项
| 问题 | 解决方案 |
|---|---|
| 触摸滚动冲突 | dragPropagation={false} |
| 元素弹回原位 | 检查 dragConstraints 和 dragElastic |
| 惯性感觉不对 | 对精确 UI 使用 dragMomentum={false} |
| 只能朝一个方向拖动 | dragElastic={{ top: 0, bottom: 0.5 }} |
滑动关闭:同时检查距离和速度——用户期望快速滑动也能生效。
无障碍
prefers-reduced-motion(必需)
import { useReducedMotion } from "motion/react"
const shouldReduce = useReducedMotion()
const variants = shouldReduce
? { opacity: 1 } // Fade only
: { opacity: 1, scale: 1, y: 0 } // Full animation
Tailwind:motion-safe:animate-pulse/motion-reduce:transition-none
最佳实践:不要禁用动画,而是简化动画。移除空间位移,保留透明度变化。
焦点管理
- 在动画开始后移动焦点:
requestAnimationFrame(() => ref.focus()) - 关闭时将焦点还原到触发元素
- 不要在
aria-live区域内添加动画
触摸目标
| 标准 | 尺寸 | Tailwind | 实际大小 |
|---|---|---|---|
| Material Design | 48×48 dp | min-h-12 min-w-12 | 约 9 mm(推荐) |
| Apple HIG | 44×44 pt | min-h-11 min-w-11 | 约 7 mm |
| WCAG 2.2 (AA) | 24×24 px | min-h-6 min-w-6 | 约 5 mm(最小值) |
为什么? 成年人的指腹平均宽度约为 9 mm。小于 7 mm 的目标会导致“胖手指”误触。跨平台应用请使用 Material Design 的 48 dp;Apple 的 44 pt 是 iOS 专用的最小值。
性能
黄金法则
- 只为
transform和opacity添加动画——由 GPU 加速 - 绝不要为以下属性添加动画:
width、height、top、left、margin、padding - 谨慎使用
will-change——仅在动画期间使用,结束后移除 - 模糊阈值:
- ≤10px:适合动画
- 11-20px:在移动设备/4K 屏幕上可能卡顿——请充分测试
-
20px:避免用于实时效果;改用预先模糊的图像
- 简单过渡优先使用 CSS,而非 JS
关键陷阱
- 高度动画:使用
layout属性,而不是animate={{ height }} - 不可见但可点击:
opacity: 0仍会接收点击——添加pointerEvents: "none" - 到处使用 will-change:会导致图层激增,并使移动设备崩溃
详细示例请参见 references/recipes.md。
示例
references/recipes.md 中按类别整理了可直接复制粘贴的模式:
- 常见 UI 模式:按钮按压、模态框进入/退出、错误抖动、交错列表、手风琴
- 触控与交互:无障碍触控目标、触控设备上的悬停、即时工具提示
- 布局动画:
layout属性、layoutId共享元素、碰撞修复 - Radix UI 集成:
forceMount模式、asChild、感知原点的弹出层 - 无障碍:焦点时机、焦点恢复、减少动态效果的变体
- 性能:高度动画(使用
layout)、不可见但可点击的修复、will-change - 退出模式:使用
forwardRef的popLayout、SSR 水合(initial={false}) - 手势:带速度检查的滑动关闭、弹性拖动边界
AnimatePresence
| 模式 | 行为 | 使用场景 |
|---|---|---|
sync(默认) | 同时进入/退出 | 交叉淡化、覆盖层 |
wait | 退出完成后再进入 | 页面过渡、标签页 |
popLayout | 退出的元素脱离布局流 | 列表项移除(配合 layout) |
退出动画陷阱
退出动画需要 AnimatePresence——没有它,卸载会立即发生:
// ❌ Exit never runs
{isOpen && <motion.div exit={{ opacity: 0 }}>...</motion.div>}
// ✅ Wrap in AnimatePresence
<AnimatePresence>
{isOpen && <motion.div exit={{ opacity: 0 }}>...</motion.div>}
</AnimatePresence>
SSR:使用 <AnimatePresence initial={false}>,以避免页面加载时播放动画。
反模式
| 不要这样做 | 应改为 | 原因 |
|---|---|---|
以 scale(0) 开始 | 使用 scale(0.9) 或更高值 | 避免“突然弹出”的效果 |
对 UI 使用 linear | 使用 ease-out 或弹簧动画 | 线性动画感觉机械 |
| 动画时长超过 500ms | 控制在 300ms 以内 | 感觉迟缓 |
| 工具提示延迟相同 | 第一次:400ms,之后:0ms | 符合用户的心理预期 |
| 忽略减少动态效果设置 | 始终使用 motion-safe: | 无障碍 |
| 动画化布局属性 | 使用 transform: scale() | 性能更好 |
| 过度弹跳 | bounce: 0-0.2 | 显得不专业 |
tailwindcss-animate
Tailwind v4:通过 CSS 中的
@theme定义关键帧,而不是在配置文件中定义。
| 类别 | 类 |
|---|---|
| 进入 | animate-in fade-in zoom-in-95 slide-in-from-top |
| 退出 | animate-out fade-out zoom-out-95 slide-out-to-top |
| 时序 | delay-150 duration-500 |
| 填充模式 | fill-mode-forwards fill-mode-backwards |
与其他技能集成
| 时机 | 技能 | 原因 |
|---|---|---|
| 实现完成后 | code-quality | 确保代码通过检查 |
| 可复用模式 | docs-write | 记录组件 API |
| 提交之前 | git-commit | 使用 feat(ui): 或 style: |
| 集成问题 | search | 查找最新模式 |
输出
- 产物:仅代码更改(不生成
.ada/输出) - 修改内容:组件动画、CSS/Tailwind 样式、Motion 配置
- 类型:工作流技能(仅提供指导,不含脚本)
参考资料
内部
references/recipes.md- 可直接复制粘贴的模式、集成示例和详细陷阱