ui-motion-guide
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