---
name: animation-motion
description: Build UI animations with correct easing, duration, and choreography. Spring physics, scroll-triggered reveals, and reduced-motion safety.
---

# Animation & Motion

Purposeful motion that communicates — not decorates

Build purposeful UI animation — motion that communicates state, guides attention, and makes interfaces feel alive without feeling slow.

## The Decision Framework

Before writing any animation code, answer these in order:

1. **Should this animate?** Not everything should. If the animation doesn't serve feedback, orientation, focus, or continuity — skip it.
2. **What is the purpose?** Feedback (button press), orientation (page transition), focus (draw eye to change), continuity (element morphs between states).
3. **What easing?** Custom curves only. CSS `ease` is too weak. Use `cubic-bezier(0.4, 0, 0.2, 1)` for exits, `cubic-bezier(0, 0, 0.2, 1)` for entrances. Never `ease-in` for UI — it starts slow, feels sluggish.
4. **How fast?** Under 300ms for micro-interactions. 150ms for hovers. 200–250ms for dropdowns and modals. 300–500ms for page transitions. Faster feels more responsive even when load time is identical.

## Spring Physics

Springs feel more natural than duration-based animations because they simulate real physics. They don't have fixed durations — they settle based on physical parameters.

- **Stiffness**: higher = snappier (100–300 for UI).
- **Damping**: higher = less bounce (15–25 for UI).
- **Bounce**: keep subtle (0.1–0.3). Avoid in most UI contexts. Reserve for drag-to-dismiss and playful interactions.
- **Interruptibility advantage**: springs maintain velocity when interrupted — CSS animations restart from zero. Ideal for gestures users might change mid-motion.

Use Motion (Framer Motion) for React: `useSpring` interpolates value changes with spring-like behavior instead of updating immediately.

## What to Animate

**Safe properties** (GPU-composited, no layout thrash):
- `transform` (translate, scale, rotate)
- `opacity`
- `filter` (blur, brightness — use sparingly)
- `clip-path` (one of the most underrated animation properties)
- `box-shadow` (for elevation changes)

**Never animate** unless truly necessary:
- `width`, `height`, `top`, `left`, `right`, `bottom`
- `margin`, `padding`
- `border-width`
- Any property that triggers layout recalculation

## Enter Animations

Use `@starting-style` for enter states:

```css
.dialog {
  opacity: 0;
  transform: scale(0.95) translateY(8px);
  transition: opacity 200ms, transform 200ms cubic-bezier(0, 0, 0.2, 1);
}
@starting-style {
  .dialog { opacity: 0; transform: scale(0.95) translateY(8px); }
}
.dialog[open] {
  opacity: 1;
  transform: scale(1) translateY(0);
}
```

**Never animate from `scale(0)`** — it creates a flash. Start from `scale(0.95)` or `scale(0.98)`.

**Make popovers origin-aware**: a popover opening from a button should transform from that button's position, not from center screen.

## Stagger Animations

Staggering items within one list is legitimate. The tell is the uniform reflex — one identical entrance applied to every section. Each reveal should fit what it reveals. Vary timing, distance, and direction based on content role.

Reveal animations must enhance an already-visible default. Don't gate content visibility on a class-triggered transition — transitions pause on hidden tabs and headless renderers, so the reveal never fires and the section ships blank.

## Scroll-Triggered Motion

Premium motion materials are not just transform/opacity. Blur, backdrop-filter, clip-path, mask, and shadow/glow are part of the palette when they materially improve the effect and stay smooth.

For scroll-linked effects:
- Use `IntersectionObserver` for triggering, not scroll position math.
- `animation-timeline: scroll()` for CSS-only scroll-driven animations (modern browsers).
- Keep scroll animations subtle — parallax of 20–40px, not 200px.

## Performance Rules

- **CSS animations beat JS under load.** Prefer CSS when the animation is declarative.
- **Use WAAPI** (Web Animations API) for programmatic CSS animations — same performance as CSS keyframes, JS control.
- **Hardware acceleration caveat**: `will-change` promotes to compositor layer but costs memory. Use for active animations, remove after.
- **`prefers-reduced-motion`** is not optional. Every animation needs an alternative: typically a crossfade or instant transition.

```css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}
```

## Component-Specific Patterns

- **Dropdowns**: 150ms, ease-out-quart. Origin from trigger button.
- **Modals**: 200ms, scale 0.95→1 + opacity. Backdrop fades separately at 150ms.
- **Toasts**: slide in from edge (translateX), 250ms spring. Exit same direction, 200ms.
- **Page transitions**: fade + subtle vertical shift (8px). 300ms max.
- **Hover states**: 100–150ms. Fast enough to feel instant, slow enough to not flash.
- **Loading spinners**: faster spin = feels faster load. 800ms rotation feels quicker than 1200ms.

## Anti-Patterns

- `ease-in` for UI animations (feels sluggish).
- Animating from `scale(0)` (visual flash).
- Bounce/elastic easing in production UI (save for playful contexts).
- Animating layout properties without `will-change` or containment.
- Uniform stagger intervals for heterogeneous content.
- Gating content visibility on reveal classes (blank page on headless render).
- Arbitrary values like `transition: all 0.3s` — specify properties explicitly.
- Ignoring `prefers-reduced-motion`.

## Verification

1. Toggle `prefers-reduced-motion` in OS settings — all animations degrade gracefully.
2. Tab through the interface — focus transitions should be visible but not distracting.
3. Rapidly click/trigger the animation multiple times — it should interrupt cleanly, not queue.
4. Profile in DevTools: no layout thrashing (purple bars in Performance tab).
5. Test on a slow device: animations should still hit 60fps or degrade to simpler alternatives.