motion-principles
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMotion Principles
动效设计原则
The foundation. Loaded by every creative skill invocation. Concise rules here. Deep-dive in.references/
基础准则。所有创意技能调用时都会加载。 此处为简洁规则,详细内容请查看目录。references/
Timing Rules
时长规则
| Context | Duration | Why |
|---|---|---|
| Micro-interaction (toggle, hover, focus) | 100-150ms | Instant feedback, no perceived delay |
| UI transition (modal, drawer, tab switch) | 200-300ms | Smooth but never sluggish |
| Page/route transition | 300-500ms | Establishes spatial narrative |
| Scroll-driven / 3D | Free (progress-based) | Tied to user input, no fixed duration |
Frequency rule: The more often an animation plays, the shorter and subtler it must be.
A button hover (1000x/day) = 100ms opacity. An onboarding reveal (1x ever) = 600ms+ full choreography.
| 场景 | 时长 | 原因 |
|---|---|---|
| 微交互(切换、悬浮、聚焦) | 100-150ms | 即时反馈,无感知延迟 |
| UI 过渡(模态框、侧边栏、标签切换) | 200-300ms | 流畅且不拖沓 |
| 页面/路由过渡 | 300-500ms | 构建空间叙事感 |
| 滚动驱动 / 3D 动画 | 无固定时长(基于进度) | 与用户输入绑定,无固定时长限制 |
频率规则: 动画播放越频繁,时长应越短、效果越柔和。
按钮悬浮(每日1000次)= 100ms 透明度动画。新手引导展示(仅1次)= 600ms+ 完整编排动画。
Easing Cheat Sheet
缓动速查表
| Action | Easing | Why |
|---|---|---|
| Element enters | | Decelerates into resting position (natural arrival) |
| Element exits | | Accelerates away (gets out of the way) |
| Element moves between states | | Smooth start and stop |
| Scroll-synced | | Matches 1:1 with input, no lag perception |
| Bouncy/playful | spring (underdamped) | Overshoot creates life |
| Snappy UI | | Fast start, smooth land |
Exit is always more subtle than enter. Enter: 300ms ease-out, full choreography. Exit: 200ms ease-in, opacity only.
| 操作 | 缓动方式 | 原因 |
|---|---|---|
| 元素进入 | | 减速至静止状态(符合自然到达逻辑) |
| 元素退出 | | 加速离开(快速让出空间) |
| 元素状态切换 | | 平滑启动与停止 |
| 滚动同步动画 | | 与输入1:1匹配,无延迟感知 |
| 弹性/活泼风格 | spring(欠阻尼) | 过冲效果赋予生命力 |
| 敏捷 UI | | 快速启动,平滑落地 |
退出动画始终比进入动画更柔和。 进入:300ms ease-out,完整编排。退出:200ms ease-in,仅透明度变化。
Native easing equivalents (cross-platform)
跨平台原生缓动等效方案
| Web (CSS / JS) | SwiftUI | Compose |
|---|---|---|
| | |
| | |
| | |
| spring (bouncy) | | |
| spring (smooth) | | |
| Web(CSS / JS) | SwiftUI | Compose |
|---|---|---|
| | |
| | |
| | |
| spring(弹性) | | |
| spring(平滑) | | |
Accessibility (Non-Negotiable)
无障碍设计(不可妥协)
Reduced motion - MANDATORY
减少动效——强制要求
Every animated component must respect the user's reduced-motion preference. No exceptions, regardless of platform.
| Platform | API |
|---|---|
| Web CSS | |
| Web JS | |
| SwiftUI | |
| UIKit | |
| Compose | Custom helper using |
CSS:
css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}SwiftUI:
swift
struct AnimatedView: View {
@Environment(\.accessibilityReduceMotion) var reduceMotion
@State var visible = false
var body: some View {
Text("Hello")
.opacity(visible ? 1 : 0)
.animation(reduceMotion ? .none : .spring(response: 0.4, dampingFraction: 0.85), value: visible)
}
}Compose:
kotlin
@Composable
fun rememberReduceMotion(): Boolean {
val context = LocalContext.current
return remember {
Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f
}
}
@Composable
fun AnimatedComponent(visible: Boolean) {
val reduce = rememberReduceMotion()
val alpha by animateFloatAsState(
targetValue = if (visible) 1f else 0f,
animationSpec = if (reduce) snap() else spring(stiffness = Spring.StiffnessMedium)
)
}每个动画组件都必须尊重用户的减少动效偏好。无论平台,无一例外。
| 平台 | API |
|---|---|
| Web CSS | |
| Web JS | |
| SwiftUI | |
| UIKit | |
| Compose | 基于 |
CSS:
css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}SwiftUI:
swift
struct AnimatedView: View {
@Environment(\.accessibilityReduceMotion) var reduceMotion
@State var visible = false
var body: some View {
Text("Hello")
.opacity(visible ? 1 : 0)
.animation(reduceMotion ? .none : .spring(response: 0.4, dampingFraction: 0.85), value: visible)
}
}Compose:
kotlin
@Composable
fun rememberReduceMotion(): Boolean {
val context = LocalContext.current
return remember {
Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f
}
}
@Composable
fun AnimatedComponent(visible: Boolean) {
val reduce = rememberReduceMotion()
val alpha by animateFloatAsState(
targetValue = if (visible) 1f else 0f,
animationSpec = if (reduce) snap() else spring(stiffness = Spring.StiffnessMedium)
)
}Other a11y requirements
其他无障碍要求
- Focus indicators must never be hidden by animations
- Animated content must meet WCAG contrast ratios at every frame (no mid-transition fading to invisible text)
- Looping animations: provide a pause mechanism
- 焦点指示器绝不能被动画遮挡
- 动画内容在每一帧都必须符合WCAG对比度标准(过渡过程中不能出现文本褪色至不可见的情况)
- 循环动画:需提供暂停机制
Universal "Do Not" Rules
通用「禁止」规则
1. Never animate width/height/top/left
1. 切勿对宽/高/top/left属性做动画
Triggers layout recalculation every frame = jank.
css
/* BAD */
.drawer { transition: height 0.3s ease; }
.drawer.open { height: 400px; }
/* GOOD */
.drawer { transition: transform 0.3s ease-out; transform: translateY(100%); }
.drawer.open { transform: translateY(0); }SwiftUI equivalent (BAD vs GOOD):
swift
// BAD - animates frame (causes layout pass)
.frame(height: open ? 400 : 0)
.animation(.easeInOut, value: open)
// GOOD - animates transform via offset
.offset(y: open ? 0 : 400)
.animation(.spring(), value: open)Compose equivalent (BAD vs GOOD):
kotlin
// BAD - animates size (full layout pass)
val height by animateDpAsState(if (open) 400.dp else 0.dp)
// GOOD - animates Y offset via graphicsLayer
val translation by animateFloatAsState(if (open) 0f else 400f)
Box(modifier = Modifier.graphicsLayer { translationY = translation })会触发每帧布局重计算,导致卡顿。
css
/* 错误示例 */
.drawer { transition: height 0.3s ease; }
.drawer.open { height: 400px; }
/* 正确示例 */
.drawer { transition: transform 0.3s ease-out; transform: translateY(100%); }
.drawer.open { transform: translateY(0); }SwiftUI 等效示例(错误 vs 正确):
swift
// 错误 - 动画修改frame(触发布局重绘)
.frame(height: open ? 400 : 0)
.animation(.easeInOut, value: open)
// 正确 - 通过offset修改transform
.offset(y: open ? 0 : 400)
.animation(.spring(), value: open)Compose 等效示例(错误 vs 正确):
kotlin
// 错误 - 动画修改尺寸(完整布局重绘)
val height by animateDpAsState(if (open) 400.dp else 0.dp)
// 正确 - 通过graphicsLayer修改Y轴偏移
val translation by animateFloatAsState(if (open) 0f else 400f)
Box(modifier = Modifier.graphicsLayer { translationY = translation })2. Never scale to 0
2. 切勿缩放至0
scale(0)css
/* BAD */
.modal-exit { transform: scale(0); }
/* GOOD */
.modal-exit { transform: scale(0.95); opacity: 0; }SwiftUI equivalent:
swift
// BAD
.scaleEffect(visible ? 1.0 : 0.0)
// GOOD
.scaleEffect(visible ? 1.0 : 0.95)
.opacity(visible ? 1.0 : 0.0)Compose equivalent:
kotlin
// BAD
Modifier.graphicsLayer { scaleX = if (visible) 1f else 0f; scaleY = if (visible) 1f else 0f }
// GOOD
Modifier.graphicsLayer {
scaleX = if (visible) 1f else 0.95f
scaleY = if (visible) 1f else 0.95f
alpha = if (visible) 1f else 0f
}scale(0)css
/* 错误示例 */
.modal-exit { transform: scale(0); }
/* 正确示例 */
.modal-exit { transform: scale(0.95); opacity: 0; }SwiftUI 等效示例:
swift
// 错误
.scaleEffect(visible ? 1.0 : 0.0)
// 正确
.scaleEffect(visible ? 1.0 : 0.95)
.opacity(visible ? 1.0 : 0.0)Compose 等效示例:
kotlin
// 错误
Modifier.graphicsLayer { scaleX = if (visible) 1f else 0f; scaleY = if (visible) 1f else 0f }
// 正确
Modifier.graphicsLayer {
scaleX = if (visible) 1f else 0.95f
scaleY = if (visible) 1f else 0.95f
alpha = if (visible) 1f else 0f
}3. Never ease-in on an entry
3. 进入动画切勿使用ease-in
Ease-in = slow start. An entering element that hesitates feels broken.
css
/* BAD */
.card-enter { animation: fadeIn 0.3s ease-in; }
/* GOOD */
.card-enter { animation: fadeIn 0.3s ease-out; }
/* OR spring via JS for natural feel */Ease-in = 缓慢启动,进入元素出现延迟会给人故障感。
css
/* 错误示例 */
.card-enter { animation: fadeIn 0.3s ease-in; }
/* 正确示例 */
.card-enter { animation: fadeIn 0.3s ease-out; }
/* 或通过JS使用spring实现自然效果 */4. Never exceed 500ms on a UI interaction
4. UI交互动画时长切勿超过500ms
Modals, dropdowns, tooltips, tabs -- users are waiting. Respect their time.
js
// BAD
gsap.to(modal, { opacity: 1, y: 0, duration: 0.8 });
// GOOD
gsap.to(modal, { opacity: 1, y: 0, duration: 0.25, ease: "power2.out" });SwiftUI:
swift
// BAD
.animation(.easeInOut(duration: 0.8), value: state)
// GOOD
.animation(.spring(response: 0.25, dampingFraction: 0.85), value: state)Compose:
kotlin
// BAD
animateContentSize(animationSpec = tween(800))
// GOOD
animateContentSize(animationSpec = spring(stiffness = Spring.StiffnessMediumLow))模态框、下拉菜单、提示框、标签页——用户在等待,请尊重他们的时间。
js
// 错误
gsap.to(modal, { opacity: 1, y: 0, duration: 0.8 });
// 正确
gsap.to(modal, { opacity: 1, y: 0, duration: 0.25, ease: "power2.out" });SwiftUI:
swift
// 错误
.animation(.easeInOut(duration: 0.8), value: state)
// 正确
.animation(.spring(response: 0.25, dampingFraction: 0.85), value: state)Compose:
kotlin
// 错误
animateContentSize(animationSpec = tween(800))
// 正确
animateContentSize(animationSpec = spring(stiffness = Spring.StiffnessMediumLow))5. Never skip prefers-reduced-motion
5. 切勿忽略prefers-reduced-motion
This is an accessibility requirement, not a nice-to-have.
js
// BAD
gsap.from('.hero-title', { opacity: 0, y: 40, duration: 0.6 });
// GOOD
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!prefersReduced) {
gsap.from('.hero-title', { opacity: 0, y: 40, duration: 0.6 });
}See the cross-platform Reduced Motion section above for SwiftUI / Compose equivalents.
这是无障碍要求,而非可选功能。
js
// 错误
gsap.from('.hero-title', { opacity: 0, y: 40, duration: 0.6 });
// 正确
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!prefersReduced) {
gsap.from('.hero-title', { opacity: 0, y: 40, duration: 0.6 });
}如需SwiftUI / Compose的等效实现,请查看上方跨平台减少动效章节。
Performance Checklist
性能检查清单
- Only animate and
transform(composite-only properties)opacity - Use sparingly and remove it after animation completes
will-change - Prefer over
requestAnimationFrame/setTimeoutfor JS animationssetInterval - For scroll-driven: CSS > JS
animation-timeline> scroll listenersIntersectionObserver - Test on low-end devices (throttle CPU 4x in DevTools)
- 仅对 和
transform属性做动画(仅需合成层的属性)opacity - 谨慎使用 ,动画结束后移除该属性
will-change - JS动画优先使用 ,而非
requestAnimationFrame/setTimeoutsetInterval - 滚动驱动动画:CSS > JS
animation-timeline> 滚动监听IntersectionObserver - 在低端设备上测试(在DevTools中把CPU节流4倍)
Quick Reference: Loading Sub-skills
快速参考:加载子技能
| Need | Load |
|---|---|
| Easing deep-dive, spring configs | |
| Copy-paste enter/exit patterns | |
| Designer-weighted style choice | |
| Mobile UX context | |
| Desktop UX context | |
| GSAP specifics | |
| Framer Motion specifics | |
| CSS-only animations | |
| Three.js / R3F | |
| Canvas / generative | |
| Compose Android animations | |
| Compose Multiplatform | |
| SwiftUI iOS/macOS animations | |
| Compose advanced graphics | |
| SwiftUI advanced graphics | |
| Visual / motion / a11y audit | |
| UI/UX intelligence (84 styles, 192 palettes) | |
| 需求 | 加载路径 |
|---|---|
| 缓动深度解析、spring配置 | |
| 可直接复用的进出动画模板 | |
| 面向设计师的风格选择指南 | |
| 移动端UX场景 | |
| 桌面端UX场景 | |
| GSAP 专属内容 | |
| Framer Motion 专属内容 | |
| 纯CSS动画 | |
| Three.js / R3F | |
| Canvas / 生成式动画 | |
| Compose Android 动画 | |
| Compose 跨平台 | |
| SwiftUI iOS/macOS 动画 | |
| Compose 高级图形 | |
| SwiftUI 高级图形 | |
| 视觉/动效/无障碍审计 | |
| UI/UX智能工具(84种风格、192种配色) | |