# Motion System — Cowork UI Kit

> Iter 40A · v0.5 · 2026-05-20
> Codified motion vocabulary: tokens, choreography, prefers-reduced-motion contract.

---

## TL;DR

Motion in this kit follows a **6-step duration scale** (instant / fast 120ms / normal 200ms / slow 320ms / slower 480ms / deliberate 800ms) crossed with **7 named easings** (linear, out, in, in-out, snap, spring, emphasized). Each token has one canonical use case backed by Material 3, Apple HIG, or Polaris research. Choreography tokens (`--motion-fade-in`, `--motion-slide-up`, ...) compose those primitives so component CSS reads as semantic intent, not raw timing. The kit honors `prefers-reduced-motion` globally — non-essential animation is replaced with instant snaps; the `.motion-essential` escape hatch preserves motion that conveys meaning (spinners, progress, skeletons, loading feedback) per WCAG 2.2 SC 2.3.3.

---

## Token table

### Duration

| Token | Value | Use case | Anti-pattern | Source |
|---|---|---|---|---|
| `--duration-instant` | 0ms | Snap with no animation (a11y fallback target) | Don't use directly — it's the value `prefers-reduced-motion` rewrites to | — |
| `--duration-fast` | 120ms | Hover lift, tap press, micro-feedback, exit transitions | Don't use for modal entry — too snappy, feels jumpy | M3 Short 2 (100ms) + Polaris fast pattern |
| `--duration-normal` | 200ms | Default: color/bg/border swaps, tooltip, dropdown, list slide | Don't use for hero element — looks reactive, not deliberate | M3 Short 4 (200ms); Apple HIG standard |
| `--duration-slow` | 320ms | Modal/drawer/sheet entry, spring confirmations | Don't use for hover — feels laggy | M3 Medium 2 (300ms) bumped slightly |
| `--duration-slower` | 480ms | Large surface change, page section reveal | Don't loop — at 480ms loops feel slow | M3 Long 1 (450ms) bumped |
| `--duration-deliberate` | 800ms | Hero moment, onboarding step, full-route transition | Don't use for inputs — blocks workflow | M3 Extra Long 1 (700ms) bumped for narrative weight |

> Why a Fibonacci-like progression (not linear ramp)? Material 3 Easing & Duration spec encodes that perceptual time scales non-linearly — small changes need fast feedback, large changes need narrative time. We follow same shape with rounder numbers (120/200/320/480/800 ≈ ratio 1.6×) so devs remember them.

### Easing

| Token | Cubic-bezier | Feels like | Use case | Source |
|---|---|---|---|---|
| `--ease-linear` | `linear` | Constant speed | Spinners, indeterminate progress, rotation | Polaris linear; M3 spec |
| `--ease-out` | `0.16, 1, 0.3, 1` | Strong deceleration; ease into rest | Enter from off-screen, modal fade-in, most exits | M3 emphasized-decel; Polaris ease-out |
| `--ease-in` | `0.7, 0, 0.84, 0` | Tense buildup; leaves fast | Exit off-screen, modal fade-out, dismissals | M3 emphasized-accel |
| `--ease-in-out` | `0.65, 0, 0.35, 1` | Symmetric — accelerate then decelerate | Accordion, tab switch, height interpolation | Apple HIG default; Polaris ease-in-out |
| `--ease-snap` | `0.4, 0, 0.2, 1` | Material standard — quick + controlled | Utility default: color/bg swap, focus ring | Material standard productive (IBM Carbon variant) |
| `--ease-spring` | `0.34, 1.56, 0.64, 1` | Playful overshoot — past 1 then settles | Success confirm, like/save tap, delight | Tailwind default ease-in-out; common spring curve |
| `--ease-emphasized` | `0.2, 0, 0, 1` | Material 3 emphasized — slow start, hard land | Hero, page route, drawer entry | M3 emphasized standard |

### Choreography (composed)

| Token | Composition | When to use |
|---|---|---|
| `--motion-fade-in` | `opacity 200ms ease-out` | Element appears in place |
| `--motion-fade-out` | `opacity 120ms ease-in` | Element disappears in place — faster than fade-in (perception) |
| `--motion-slide-up` | `transform 200ms ease-out` | Toast, drawer-bottom, content entry |
| `--motion-slide-down` | `transform 120ms ease-in` | Dismiss downward |
| `--motion-scale-in` | `transform 200ms ease-out` | Tooltip/popover scale 0.95 → 1 |
| `--motion-scale-out` | `transform 120ms ease-in` | Dismiss with scale |
| `--motion-blur-in` | `filter 320ms ease-out` | Backdrop blur on overlay |
| `--motion-color` | `color 120ms ease-snap` | Text color swap (link hover, status flip) |
| `--motion-bg` | `background-color 120ms ease-snap` | Button bg hover |
| `--motion-shadow` | `box-shadow 200ms ease-out` | Card lift |

### Displacement

| Token | Value | Use case |
|---|---|---|
| `--motion-distance-sm` | 4px | Tooltip nudge, micro-shift on hover |
| `--motion-distance-md` | 8px | Dropdown menu offset |
| `--motion-distance-lg` | 16px | Toast slide, mobile bottom sheet |
| `--motion-distance-xl` | 32px | Page transition, full hero motion |

### Stagger

| Token | Value | Use case |
|---|---|---|
| `--motion-stagger-delay` | 50ms | Delay between list-item entry — orchestrated, not slow |

---

## Decision tree — choosing the right duration

```
How important is the element being animated?

┌─ Trivial micro-feedback (hover, focus, tap)
│   → --duration-fast (120ms)
│   Rule: should feel "instant but visible"
│
├─ Standard UI swap (color/bg, tooltip, dropdown)
│   → --duration-normal (200ms)   ← DEFAULT — when in doubt
│
├─ Surface entering view (modal, drawer, sheet)
│   → --duration-slow (320ms)
│   Pair with ease-emphasized for focal moments
│
├─ Large surface change (panel swap, page section)
│   → --duration-slower (480ms)
│
└─ Hero / narrative moment (route change, onboarding)
    → --duration-deliberate (800ms)
    Pair with --motion-distance-xl + ease-emphasized

Is this element looping (spinner/progress/skeleton)?
    → --ease-linear (perception: constant speed)
    → Add .motion-essential to keep it running for reduce-motion users
```

> Asymmetry rule (M3 + Apple convention): **exits are faster than entries** because removing something needs less attention than introducing something. Default `--duration-fast` for exit + `--duration-normal` for enter is the kit's standard pair.

---

## Easing semantics — which curve for which feeling

- **out** — "the world is calming down." Element decelerates into rest. Default for arrivals.
- **in** — "the world is tensing up." Element accelerates as it leaves. Default for departures.
- **in-out** — "shared motion path." Both ends slow, middle fast. For things changing shape (accordion, tab indicator).
- **snap** — "utility, no opinion." Material standard. For color/bg flips where you don't want personality.
- **spring** — "the system is happy for you." Overshoots and settles. For confirmations, completions, delights.
- **emphasized** — "this matters." Slow start makes user notice; hard land feels arrival. For hero / route.
- **linear** — "mechanical." No personality. For physical metaphors only (rotation, fill).

---

## Choreography patterns — composition rules

### Stagger
List items entering should be delayed `--motion-stagger-delay` apart. After 5-6 items, decay or cut off — staggering 20 items feels glacial. Pattern:
```css
.item:nth-child(1) { animation-delay: 0ms; }
.item:nth-child(2) { animation-delay: calc(var(--motion-stagger-delay) * 1); }
/* ... up to ~5 then cap */
```

### Layer (modal + backdrop)
The supporting layer should start first or simultaneously. Backdrop fades in over `--duration-slow`, dialog scales in over same duration with `--ease-emphasized`. Don't make the dialog wait — feels laggy.

### Sequence (form submit → loading → success)
Three discrete states, each with its own motion. Transitions between them use `--duration-normal`. The success state uses `--ease-spring` for delight.

### Parallax / scroll-linked
**Avoid unless brand explicitly calls for it.** WCAG 2.3.3 cites parallax as a canonical non-essential animation that must be disable-able. If you ship it, gate behind `@media (prefers-reduced-motion: no-preference)`.

---

## prefers-reduced-motion contract

The kit installs a global override in `tokens/semantic.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;
  }
  .motion-essential, .motion-essential * { /* restore */ }
}
```

**Effect:** every transition + animation is collapsed to 0.01ms (effectively instant). The user still gets correct state — just without movement.

### Escape hatch — `.motion-essential`

Add this class to elements whose motion **conveys meaning, not decoration**. Per WCAG SC 2.3.3, essential motion is exempt. Canonical cases:

| Pattern | Why essential |
|---|---|
| Loading spinner | Communicates "system busy" — without rotation, looks frozen |
| Progress bar | Communicates "X% done" — animation IS the data |
| Skeleton shimmer | Signals "this is a placeholder, not real content" |
| Drag preview | Communicates "this is moving with my finger" |
| Newly-loaded content slide-in | Communicates "fresh data arrived" (debatable — discuss case-by-case) |

**Anti-pattern:** slapping `.motion-essential` on every animation. The kit's reduced-motion respect is a feature, not a bug. If you find yourself wanting to mark hover lift as essential, you don't — drop it for reduce users.

---

## Anti-patterns

1. **Gratuitous animation** — animating elements that don't need to move (e.g. static text fading in just for "polish"). Adds time without information.
2. **Motion that competes with user** — long-running animation on a button the user just pressed. User has moved on; the motion is now annoying.
3. **Ignoring system preferences** — shipping CSS animations without `@media (prefers-reduced-motion: reduce)`. The kit's global override catches this, but custom keyframes outside the kit still need awareness.
4. **Long durations on small distances** — `transform: translateX(2px)` over 480ms feels broken. Match duration to distance.
5. **ease-in on arrivals** — arriving elements should decelerate, not accelerate. Backwards motion feels wrong.
6. **Symmetric duration enter/exit** — equal time looks slower. Exits should be ~60% of enter duration (kit's 120/200 pair encodes this).
7. **Hard-coded ms in component CSS** — use tokens. If a token doesn't match your need, propose a new one in `foundation.css` rather than inlining.

---

## References

All sources studied for this iteration. Inline links match the tokens they informed.

- [Material Design 3 — Motion overview](https://m3.material.io/styles/motion/overview)
- [Material Design 3 — Easing and duration tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs)
- [Apple HIG — Motion](https://developer.apple.com/design/human-interface-guidelines/motion)
- [web.dev — prefers-reduced-motion](https://web.dev/articles/prefers-reduced-motion)
- [WCAG 2.2 SC 2.3.3 — Animation from Interactions](https://www.w3.org/WAI/WCAG22/Understanding/animation-from-interactions.html)
- [Polaris (Shopify) — Motion tokens](https://polaris-react.shopify.com/tokens/motion)
- [IBM Carbon — Motion overview](https://carbondesignsystem.com/elements/motion/overview/)
- [Tailwind CSS — transition-timing-function (v4)](https://tailwindcss.com/docs/transition-timing-function)
- [Motion (Framer) — React docs](https://motion.dev/docs/react-motion-component)

---

## See also

- 📁 [tokens/foundation.css](../tokens/foundation.css) — token definitions (extends here)
- 📁 [tokens/semantic.css](../tokens/semantic.css) — reduced-motion global block
- 📄 [INDEX.md](../INDEX.md) — kit-level index
- 📄 [A11Y-RULES.md](../A11Y-RULES.md) — accessibility rules including motion
