# Accessibility & Modern Motion APIs

## `prefers-reduced-motion` — collapse, don't disable

```css
:root {
  --motion-duration-fast: 150ms;
  --motion-duration-base: 250ms;
  --motion-duration-slow: 400ms;
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --motion-duration-fast: 0.01ms;
    --motion-duration-base: 0.01ms;
    --motion-duration-slow: 0.01ms;
  }
}
```

**Why 0.01ms, not 0?** Some Safari/WebKit versions skip the `transitionend`
event entirely when duration is exactly `0` — state machines that wait on
that event hang. `0.01ms` is functionally instant but keeps the event
pipeline intact.

**What stays vs. what goes** when reduced-motion is requested:

- KEEP: opacity fades < 200ms (no vestibular impact), color cross-fades,
  scale changes < 5%, subtle elevation changes.
- COLLAPSE: translate / parallax / scale > 5% / rotate / skew / 3D
  perspective / spring overshoot — anything with apparent motion across the
  viewport.

**Don't** apply a global `* { animation: none !important }` reset — that
breaks loading spinners, focus indicators, and any animation that
*communicates state*. Token-level collapse is the right primitive.

## View Transitions API

For same-document navigation (SPA route change, list ↔ detail, tab switch):

```js
function navigate(to) {
  if (!document.startViewTransition) {
    applyDom(to); // graceful fallback — no transition
    return;
  }
  document.startViewTransition(() => applyDom(to));
}
```

For shared-element morph (hero image survives the route change):

```css
.hero-image {
  view-transition-name: hero-image; /* unique per shared element */
}

::view-transition-old(hero-image),
::view-transition-new(hero-image) {
  animation-duration: var(--motion-duration-base);
  animation-timing-function: cubic-bezier(0.2, 0, 0, 1); /* emphasized */
}
```

For cross-document transitions (MPA, Chrome 126+, behind opt-in CSS):

```css
@view-transition { navigation: auto; }
```

**Accessibility**: View Transitions automatically respect
`prefers-reduced-motion` when the page exposes the reduced-motion media
query — no extra wiring needed beyond the token collapse above.

**Use it for**: navigation, list ↔ detail morphs, sort / filter reorders,
modal hero reveal. **Don't use it for**: per-frame interactive feedback
(hover, focus, drag) — those need direct CSS / spring animations.

## Spring vs. timing function — when to pick which

| Use case | Pick |
|---|---|
| Direct manipulation (drag release, swipe, pull-to-refresh) | Spring |
| Page / view transition | Cubic-bezier (deterministic, easy to choreograph) |
| Acknowledgment / tap feedback | Cubic-bezier (short, predictable) |
| Interruptible animation that should "settle naturally" | Spring |
| Stagger choreography | Cubic-bezier (timeline easier to reason about) |

**Spring rule of thumb**: stiffness 250–350 + damping 18–24 = "standard
feel". Higher stiffness = snappier; lower damping = more oscillation. Cap
total settle time at 600ms — beyond that the user perceives "lag", not
physicality.

## Choreography budget

- Single-element transition: 100–500ms (see `timing-easing-tables.md`).
- Stagger of multiple children: each child delay 30–50ms, **total budget
  ≤ 600ms** (faster on dense lists — cap at 400ms for >10 items).
- Page-load sequence: hero ≤ 250ms, secondary content stagger ≤ 400ms
  total. Beyond that the user is staring at incomplete UI.

## Animation that communicates state — never collapse these

Reduced-motion does NOT mean *no feedback*. Always preserve:

- Focus ring appearance (instant is fine — but visible).
- Loading indicators (a static spinner or skeleton is still feedback).
- Validation error appearance (color + icon — duration can be 0).
- Toast / notification appearance (fade-in opacity ≤ 200ms is acceptable
  under reduced-motion).

Stripping these leaves users without feedback that an action registered —
worse than the animation they tried to avoid.
