返回 Skill 列表
extension
分类: 开发与工程无需 API Key

ui-motion-guide

UI动效和动画原则指南,适用于网页/应用程序界面。涵盖弹簧与缓动决策框架、时间/物理规则、AnimatePresence退出模式、变形图标以及容器边界动画等内容。在实现或审查动画、过渡效果、动效设计时使用,或者当用户提到“动画”、“动效”、“弹簧”、“缓动”、“AnimatePresence”、“变形图标”、“容器动画”、“UI动效”、“过渡”等术语时参考本指南。不适用于仅涉及CSS样式的模式(请参阅ui-css-patterns)或声音反馈(请参阅ui-sound-design)。

person作者: jakexiaohubgithub

UI Motion Guide

Unified reference for UI motion/animation principles and implementation patterns. Based on Disney's 12 Principles adapted for digital interfaces, spring/easing physics, and Motion (Framer Motion) library patterns.

12 Principles of Animation (UI Adaptation)

| # | Principle | UI Takeaway | |---|-----------|-------------| | 1 | Squash & Stretch | Subtle scale deformation (0.95-1.05). Too much = cartoon. | | 2 | Anticipation | Prepare user for next action (button compress before send, pull-to-refresh). | | 3 | Staging | One focal point at a time. Dim backgrounds for modals. Sequence reveals. | | 4 | Straight Ahead / Pose to Pose | Web = pose to pose (keyframes, browser interpolates). Context menus: animate exit only, never entrance. | | 5 | Follow Through / Overlapping | Nothing moves as single rigid unit. Springs add organic overshoot. Stagger ≤50ms/item. | | 6 | Slow In & Slow Out | ease-out for entrances, ease-in for exits, ease-in-out for deliberate moves. | | 7 | Arcs | Curved paths feel organic (Dynamic Island). Reserve for hero moments. | | 8 | Secondary Action | Flourishes supporting main action (sparkle on checkmark). Sound can qualify. | | 9 | Timing | Keep interactions ≤300ms. Be consistent. Define timing scale early. | | 10 | Exaggeration | Amplify for emphasis. Good for onboarding, empty states, errors. Sparingly. | | 11 | Solid Drawing | Shadows = depth, layering = hierarchy. CSS perspective for 3D transforms. | | 12 | Appeal | Sum of all techniques applied with care. |


Spring vs Easing Decision Framework

This is the core decision model. Ask: "Is this motion reacting to the user, or is the system speaking?"

┌─────────────────────────┬───────────────┬──────────────────────────────────┐
│ Scenario                │ Use           │ Why                              │
├─────────────────────────┼───────────────┼──────────────────────────────────┤
│ Gesture-driven motion   │ Spring        │ Survives interruption,           │
│ (drag, flick, swipe)    │               │ preserves velocity               │
├─────────────────────────┼───────────────┼──────────────────────────────────┤
│ System state change     │ Easing curve  │ Clear start/end, predictable     │
│ (toggle, page swap)     │               │                                  │
├─────────────────────────┼───────────────┼──────────────────────────────────┤
│ Time representation     │ Linear        │ 1:1 time-progress relationship   │
│ (progress, loading bar) │               │                                  │
├─────────────────────────┼───────────────┼──────────────────────────────────┤
│ High-frequency input    │ None          │ Animation adds noise             │
│ (typing, fast toggles)  │               │                                  │
└─────────────────────────┴───────────────┴──────────────────────────────────┘

Easing Curves

Format: cubic-bezier(x1, y1, x2, y2) — x1,y1 control responsiveness (how motion begins), x2,y2 control how motion ends.

Common patterns:

  • Entrance (ease-out): cubic-bezier(0, 0, 0.2, 1) — snappy arrival
  • Exit (ease-in): cubic-bezier(0.4, 0, 1, 1) — builds momentum leaving
  • In-view transition (ease-in-out): cubic-bezier(0.4, 0, 0.2, 1) — deliberate move
  • Linear: Only for progress indicators, never for motion

Spring Parameters

| Parameter | What it controls | Effect | |-----------|-----------------|--------| | stiffness | How strongly spring pulls toward target | Higher = snappier | | damping | How quickly energy is removed | Higher = less bounce | | mass | How heavy the object feels | Higher = more sluggish |

Key difference: springs have no predefined end time (they settle naturally), easing curves do.

Rule: Shorten duration first before adjusting the curve. If a curve feels wrong, it's usually too long.


Timing & Duration Rules

| Context | Duration | Rule ID | |---------|----------|---------| | Press / hover feedback | 120-180ms | duration-press-hover | | Small state change (toggle, chip) | 180-260ms | duration-small-state | | User-initiated max | 300ms | duration-max-300ms | | Context menu entrance | 0ms (instant) | none-context-menu-entrance | | Keyboard navigation | 0ms (instant) | none-keyboard-navigation |


Rules with Examples

Easing Direction

easing-entrance-ease-out — Entrances use ease-out (fast start, gentle stop).

// FAIL: Linear entrance feels robotic
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.2, ease: "linear" }}
/>

// PASS: ease-out entrance feels snappy
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.2, ease: [0, 0, 0.2, 1] }}
/>

easing-exit-ease-in — Exits use ease-in (slow start, fast departure).

// FAIL: ease-out on exit feels like it's hesitating to leave
<motion.div
  exit={{ opacity: 0, y: -20 }}
  transition={{ duration: 0.15, ease: "easeOut" }}
/>

// PASS: ease-in exit builds momentum
<motion.div
  exit={{ opacity: 0, y: -20 }}
  transition={{ duration: 0.15, ease: [0.4, 0, 1, 1] }}
/>

Physics & Active States

physics-active-state — Interactive elements need :active / whileTap with scale.

// FAIL: Button with no press feedback
<motion.button onClick={handleSubmit}>
  Submit
</motion.button>

// PASS: Button with tactile scale response
<motion.button
  onClick={handleSubmit}
  whileTap={{ scale: 0.97 }}
  transition={{ type: "spring", stiffness: 500, damping: 30 }}
>
  Submit
</motion.button>

physics-subtle-deformation — Scale values in 0.95-1.05 range.

// FAIL: Exaggerated scale feels cartoonish
whileTap={{ scale: 0.8 }}

// PASS: Subtle deformation
whileTap={{ scale: 0.97 }}
whileHover={{ scale: 1.02 }}

physics-no-excessive-stagger — Stagger delay ≤50ms per item.

// FAIL: 200ms stagger makes list feel sluggish
const variants = {
  visible: { transition: { staggerChildren: 0.2 } },
};

// PASS: 30ms stagger feels brisk
const variants = {
  visible: { transition: { staggerChildren: 0.03 } },
};

Spring Usage

spring-for-gestures — Gesture-driven motion must use springs.

// FAIL: Easing for drag (can't preserve velocity on release)
<motion.div
  drag="x"
  animate={{ x: 0 }}
  transition={{ duration: 0.3, ease: "easeOut" }}
/>

// PASS: Spring survives interruption and preserves velocity
<motion.div
  drag="x"
  animate={{ x: 0 }}
  transition={{ type: "spring", stiffness: 300, damping: 25 }}
/>

spring-params-balanced — Avoid excessive oscillation (high stiffness + low damping).

// FAIL: Bounces forever
transition={{ type: "spring", stiffness: 800, damping: 5 }}

// PASS: Controlled overshoot-and-settle
transition={{ type: "spring", stiffness: 300, damping: 25 }}

Staging

staging-one-focal-point — One prominent animation at a time.

// FAIL: Multiple competing animations
<motion.div animate={{ scale: 1.1, rotate: 10 }} />
<motion.div animate={{ x: 100 }} />
<motion.div animate={{ opacity: [0, 1, 0] }} />

// PASS: Single focal animation, others static or subtle
<motion.div animate={{ scale: 1.1 }} />  {/* Hero action */}
<div className="opacity-50" />             {/* Dimmed background */}

staging-dim-background — Dim overlay for modals/dialogs.

// PASS: Overlay dims background to direct focus
<motion.div
  className="fixed inset-0 bg-black/40"
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  exit={{ opacity: 0 }}
/>

No-Animation Cases

none-high-frequency — No animation for high-frequency interactions.

// FAIL: Animating every keystroke
<motion.span animate={{ opacity: 1 }} key={inputValue}>
  {inputValue}
</motion.span>

// PASS: Instant update
<span>{inputValue}</span>

Accessibility

Always respect prefers-reduced-motion:

// Global: disable all motion
const MotionConfig = ({ children }) => (
  <LazyMotion features={domAnimation}>
    <MotionConfig reducedMotion="user">{children}</MotionConfig>
  </LazyMotion>
);

// Per-element: provide fallback
const prefersReduced = useReducedMotion();
<motion.div
  animate={{ x: prefersReduced ? 0 : 100, opacity: 1 }}
/>

Reference Documents

For detailed implementation patterns, load the relevant reference:

| Reference | Content | When to Load | |-----------|---------|--------------| | references/animate-presence.md | AnimatePresence exit animations, modes, usePresence, nested exits | Implementing enter/exit transitions, list animations, route transitions | | references/morphing-icons.md | SVG line-based morphing icon system with spring physics | Building animated icon sets, hamburger-to-X transitions | | references/container-animation.md | Animating auto width/height, useMeasure, fluid text | Expanding panels, accordions, dynamic-width buttons, text transitions |


Output Format

When reviewing motion code, report issues as:

[RULE_ID] file:line — description

Example:
[easing-entrance-ease-out] components/Modal.tsx:42 — Entrance uses linear easing; should use ease-out
[duration-max-300ms] components/Drawer.tsx:18 — Transition duration is 500ms; reduce to ≤300ms
[spring-for-gestures] components/SwipeCard.tsx:31 — Drag interaction uses easing; switch to spring

Quick Reference

ENTRANCES:  ease-out    [0, 0, 0.2, 1]        120-300ms
EXITS:      ease-in     [0.4, 0, 1, 1]        120-200ms
IN-VIEW:    ease-in-out [0.4, 0, 0.2, 1]      180-300ms
PROGRESS:   linear      —                      matches real time
GESTURES:   spring      stiffness/damping/mass no fixed duration
PRESS:      spring      stiffness:500 damp:30  whileTap scale:0.97
HIGH-FREQ:  none        —                      instant
STAGGER:    ≤50ms/item  —                      30ms typical