motion-principles

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Motion Principles

动效设计原则

The foundation. Loaded by every creative skill invocation. Concise rules here. Deep-dive in
references/
.

基础准则。所有创意技能调用时都会加载。 此处为简洁规则,详细内容请查看
references/
目录。

Timing Rules

时长规则

ContextDurationWhy
Micro-interaction (toggle, hover, focus)100-150msInstant feedback, no perceived delay
UI transition (modal, drawer, tab switch)200-300msSmooth but never sluggish
Page/route transition300-500msEstablishes spatial narrative
Scroll-driven / 3DFree (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

缓动速查表

ActionEasingWhy
Element enters
ease-out
/ spring
Decelerates into resting position (natural arrival)
Element exits
ease-in
Accelerates away (gets out of the way)
Element moves between states
ease-in-out
Smooth start and stop
Scroll-synced
linear
/
none
Matches 1:1 with input, no lag perception
Bouncy/playfulspring (underdamped)Overshoot creates life
Snappy UI
cubic-bezier(0.2, 0, 0, 1)
Fast start, smooth land
Exit is always more subtle than enter. Enter: 300ms ease-out, full choreography. Exit: 200ms ease-in, opacity only.
操作缓动方式原因
元素进入
ease-out
/ spring
减速至静止状态(符合自然到达逻辑)
元素退出
ease-in
加速离开(快速让出空间)
元素状态切换
ease-in-out
平滑启动与停止
滚动同步动画
linear
/
none
与输入1:1匹配,无延迟感知
弹性/活泼风格spring(欠阻尼)过冲效果赋予生命力
敏捷 UI
cubic-bezier(0.2, 0, 0, 1)
快速启动,平滑落地
退出动画始终比进入动画更柔和。 进入:300ms ease-out,完整编排。退出:200ms ease-in,仅透明度变化。

Native easing equivalents (cross-platform)

跨平台原生缓动等效方案

Web (CSS / JS)SwiftUICompose
cubic-bezier(0.2, 0, 0, 1)
.spring(response: 0.4, dampingFraction: 0.85)
or
.snappy
spring(stiffness = Spring.StiffnessMedium, dampingRatio = 0.85f)
ease-out
.easeOut(duration: 0.3)
tween(durationMillis = 300, easing = LinearOutSlowInEasing)
ease-in
.easeIn(duration: 0.2)
tween(durationMillis = 200, easing = FastOutLinearInEasing)
spring (bouncy)
.bouncy
(iOS 17+)
spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)
spring (smooth)
.smooth
(iOS 17+)
spring(stiffness = Spring.StiffnessMedium, dampingRatio = Spring.DampingRatioNoBouncy)

Web(CSS / JS)SwiftUICompose
cubic-bezier(0.2, 0, 0, 1)
.spring(response: 0.4, dampingFraction: 0.85)
.snappy
spring(stiffness = Spring.StiffnessMedium, dampingRatio = 0.85f)
ease-out
.easeOut(duration: 0.3)
tween(durationMillis = 300, easing = LinearOutSlowInEasing)
ease-in
.easeIn(duration: 0.2)
tween(durationMillis = 200, easing = FastOutLinearInEasing)
spring(弹性)
.bouncy
(iOS 17+)
spring(stiffness = Spring.StiffnessLow, dampingRatio = Spring.DampingRatioMediumBouncy)
spring(平滑)
.smooth
(iOS 17+)
spring(stiffness = Spring.StiffnessMedium, dampingRatio = Spring.DampingRatioNoBouncy)

Accessibility (Non-Negotiable)

无障碍设计(不可妥协)

Reduced motion - MANDATORY

减少动效——强制要求

Every animated component must respect the user's reduced-motion preference. No exceptions, regardless of platform.
PlatformAPI
Web CSS
@media (prefers-reduced-motion: reduce)
Web JS
window.matchMedia('(prefers-reduced-motion: reduce)')
SwiftUI
@Environment(\.accessibilityReduceMotion) var reduceMotion
UIKit
UIAccessibility.isReduceMotionEnabled
(+
reduceMotionStatusDidChangeNotification
)
ComposeCustom helper using
Settings.Global.ANIMATOR_DURATION_SCALE
(deep-dive in
mobile-principles/references/accessibility-mobile.md
)
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
@media (prefers-reduced-motion: reduce)
Web JS
window.matchMedia('(prefers-reduced-motion: reduce)')
SwiftUI
@Environment(\.accessibilityReduceMotion) var reduceMotion
UIKit
UIAccessibility.isReduceMotionEnabled
(+
reduceMotionStatusDidChangeNotification
Compose基于
Settings.Global.ANIMATOR_DURATION_SCALE
的自定义工具类(详细内容查看
mobile-principles/references/accessibility-mobile.md
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)
causes elements to vanish in a black hole. Always keep a minimum.
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
    transform
    and
    opacity
    (composite-only properties)
  • Use
    will-change
    sparingly and remove it after animation completes
  • Prefer
    requestAnimationFrame
    over
    setTimeout
    /
    setInterval
    for JS animations
  • For scroll-driven: CSS
    animation-timeline
    > JS
    IntersectionObserver
    > scroll listeners
  • Test on low-end devices (throttle CPU 4x in DevTools)

  • 仅对
    transform
    opacity
    属性做动画(仅需合成层的属性)
  • 谨慎使用
    will-change
    ,动画结束后移除该属性
  • JS动画优先使用
    requestAnimationFrame
    ,而非
    setTimeout
    /
    setInterval
  • 滚动驱动动画:CSS
    animation-timeline
    > JS
    IntersectionObserver
    > 滚动监听
  • 在低端设备上测试(在DevTools中把CPU节流4倍)

Quick Reference: Loading Sub-skills

快速参考:加载子技能

NeedLoad
Easing deep-dive, spring configs
references/easing-guide.md
Copy-paste enter/exit patterns
references/enter-exit-recipes.md
Designer-weighted style choice
references/designers.md
Mobile UX context
../mobile-principles/SKILL.md
Desktop UX context
../desktop-principles/SKILL.md
GSAP specifics
../gsap/SKILL.md
Framer Motion specifics
../framer-motion/SKILL.md
CSS-only animations
../css-native/SKILL.md
Three.js / R3F
../threejs-r3f/SKILL.md
Canvas / generative
../canvas-generative/SKILL.md
Compose Android animations
../compose-motion/SKILL.md
Compose Multiplatform
../compose-multiplatform/SKILL.md
SwiftUI iOS/macOS animations
../swiftui-motion/SKILL.md
Compose advanced graphics
../compose-graphics/SKILL.md
SwiftUI advanced graphics
../swiftui-graphics/SKILL.md
Visual / motion / a11y audit
../design-audit/SKILL.md
UI/UX intelligence (84 styles, 192 palettes)
../ui-ux-pro-max/SKILL.md
需求加载路径
缓动深度解析、spring配置
references/easing-guide.md
可直接复用的进出动画模板
references/enter-exit-recipes.md
面向设计师的风格选择指南
references/designers.md
移动端UX场景
../mobile-principles/SKILL.md
桌面端UX场景
../desktop-principles/SKILL.md
GSAP 专属内容
../gsap/SKILL.md
Framer Motion 专属内容
../framer-motion/SKILL.md
纯CSS动画
../css-native/SKILL.md
Three.js / R3F
../threejs-r3f/SKILL.md
Canvas / 生成式动画
../canvas-generative/SKILL.md
Compose Android 动画
../compose-motion/SKILL.md
Compose 跨平台
../compose-multiplatform/SKILL.md
SwiftUI iOS/macOS 动画
../swiftui-motion/SKILL.md
Compose 高级图形
../compose-graphics/SKILL.md
SwiftUI 高级图形
../swiftui-graphics/SKILL.md
视觉/动效/无障碍审计
../design-audit/SKILL.md
UI/UX智能工具(84种风格、192种配色)
../ui-ux-pro-max/SKILL.md