# Animation Quality

> Audit and improve animations for 60fps, prefers-reduced-motion compliance, motion-feel (duration/easing), and accessibility. Use when jank appears in scroll/interaction, INP regresses, an animation auto-plays/loops or flashes, or before shipping. Not for diagnosing broad Core Web Vitals regressions (use rendering-performance) or full WCAG conformance auditing (use accessibility-audit).

- Skill: `jaykim88/animation-quality` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jaykim88/animation-quality`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jaykim88/animation-quality/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: JayKim88 (https://skillmd.com/u/jaykim88)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/jaykim88/animation-quality

---


# Animation Quality

## Purpose
All animations stay on the compositor thread (60fps), respect user motion preferences, and use the right tool (CSS vs JS) for the job.

**Universal** — GPU-compositor rules (transform/opacity only), `prefers-reduced-motion` compliance, CSS-vs-JS choice, and 60fps target are browser-level concerns identical across frameworks.

## Procedure

1. **Audit animated properties — restrict to compositor-friendly props**
   - First, does each animation **earn its place**? Motion should communicate state change, spatial continuity, or feedback — cut decorative motion that only delays the user.
   - Allowed: `transform` (translate/scale/rotate/skew), `opacity`, `filter`
   - Banned: `top`, `left`, `width`, `height`, `margin`, `padding` (trigger layout/paint)
   - Animating layout also shifts surrounding content → **CLS**. Entrance/expand animations (accordions, `height: auto`) are the classic offender — reserve the space, or animate `transform` / `grid-template-rows: 0fr→1fr` instead of `height`
   - Search: `grep -rE 'transition.*?(width|height|top|left|margin|padding)' src/`

2. **Respect `prefers-reduced-motion` globally**
   - Single source of truth at the app root — opt the whole app into the user's motion preference. (see Implementation; Framer Motion)
   - **Scope clarification**: the global "user-preference" mode disables `transform` and `layout` animations while *preserving* opacity and color transitions — not all motion is killed. This is the intended behavior (preserves accessibility-safe feedback).
   - Allow per-component overrides where a critical affordance must still move (e.g., an onboarding cue): detect the preference and conditionally adjust animation props
   - Cover decorative animations outside the motion library too (e.g., a CSS-level reduced-motion variant — see Implementation)
   - **Auto-playing or looping motion** that runs >5s alongside other content must be pausable/stoppable — carousels, looping backgrounds, marquees (WCAG 2.2.2)
   - **Nothing flashes more than 3×/second** — avoid rapid flash/strobe effects entirely (WCAG 2.3.1, seizure risk)
   - Verify in OS accessibility settings (e.g., macOS System Preferences → Accessibility → Display → Reduce motion)

3. **Audit `will-change` usage**
   - `will-change` should be applied *just before* animation, removed after
   - Permanent `will-change` on many elements degrades performance
   - Use sparingly; default is no `will-change`

4. **Choose the right tool — and tune the feel**
   - CSS transition: simple state changes (hover, focus, open/close)
   - Framer Motion / Motion: physics, gestures, complex sequences, layout animations
   - **Duration**: micro-interactions 100–200ms, most UI transitions 200–300ms, larger/page transitions ≤ ~500ms — too-slow motion feels sluggish and hurts perceived performance
   - **Easing**: ease-out for entrances (decelerate in), ease-in for exits, ease-in-out for moves; avoid `linear` for UI motion (reserve it for continuous/looping)

5. **Use the animation library's primitives correctly**
   - Animate unmount/exit transitions — easy to forget
   - Prefer the lib's layout-animation over manual position math for size/position changes
   - Ensure shared-element transitions have no orphans (every entry has a matching exit). (see Implementation; Framer `AnimatePresence` / `layout` / `layoutId`)

6. **Scroll-driven animations**
   - Prefer `IntersectionObserver` or Motion's `useScroll`
   - Avoid `scroll` event listeners (run on main thread)

7. **JS animations use `requestAnimationFrame`**
   - Never `setTimeout` for animation loops
   - Cancel `rAF` on cleanup

8. **Verify (validation loop)**
   - Chrome DevTools → Performance: record the interaction; look for dropped frames, layout thrashing, paint regions
   - If not 60fps / 0 dropped frames, find the non-compositor property or main-thread work and return to step 1 / step 7
   - Confirm interactions stay responsive *during* animation (INP unaffected — heavy JS animation on the main thread blocks input)
   - Toggle OS "Reduce motion" and confirm the app responds; confirm nothing flashes >3×/s
   - Loop until: 60fps + 0 dropped frames + reduced-motion honored

## Severity tiers

| Tier | Examples | Action SLA |
|---|---|---|
| **Critical** | Content flashes >3×/second (seizure risk, WCAG 2.3.1); `prefers-reduced-motion` ignored entirely; auto-playing motion >5s with no pause control (WCAG 2.2.2) | Block release; fix immediately |
| **Major** | Animating layout props (`top`/`width`/`height`) causing jank or CLS; dropped frames in the Performance trace; permanent `will-change` on many elements | Fix this sprint |
| **Minor** | Sluggish duration / wrong easing; missing exit (`AnimatePresence`) animations; `scroll` listener instead of `IntersectionObserver` | Schedule within 2 sprints |

## Completion Criteria
- [ ] Animated properties limited to `transform`/`opacity`/`filter`
- [ ] `prefers-reduced-motion` honored globally
- [ ] No content flashes >3×/s; auto-playing motion >5s is pausable (WCAG 2.3.1 / 2.2.2)
- [ ] No animation shifts surrounding layout (no CLS from motion)
- [ ] Dropped frames = 0 in DevTools Performance trace
- [ ] No `will-change` permanent declarations
- [ ] `AnimatePresence` covers all conditional renders with exit animations

## Output
- **Animation code**: refactored to compositor-only properties (transform / opacity / filter)
- **Reduced-motion config**: global `<MotionConfig reducedMotion="user">` (or framework equivalent) wired at app root
- **Performance trace**: Chrome DevTools Performance recording showing 60fps + 0 dropped frames, saved as `docs/perf-trace-YYYY-MM-DD.json`
- **Animation inventory** (paste into PR): list of animations with type (decorative / functional / scroll-driven) and library used
- **Commit format**: `fix(anim): switch <component> to transform/opacity` for fixes; `feat(anim): add <interaction>` for new

## Implementation

### React + Next.js (default)
- Library: Motion (formerly Framer Motion) — `<MotionConfig reducedMotion="user">`, `useReducedMotion()` hook, `<AnimatePresence>`, `layout` + `layoutId`
- Tailwind reduced-motion variant: `motion-reduce:transition-none`
- CSS transitions for simple state changes; Motion for physics / gestures / layout

### Other stacks
- **Vue / Nuxt**: `<Transition>` / `<TransitionGroup>` built-in; `@vueuse/motion` for spring physics; `prefers-reduced-motion` via Tailwind variant or CSS
- **SvelteKit**: built-in `transition:` directive (`fade`, `fly`, `scale`, `crossfade`) + `motion` package; `prefers-reduced-motion` store available via `@svelte-put/preferences`
- **Angular**: `@angular/animations` module with `trigger()`, `state()`, `animate()`; `BreakpointObserver` for reduced-motion media query
- **Universal**: GPU-compositor rules (transform/opacity only, avoid top/left/width/height) work in every browser; `prefers-reduced-motion` is a CSS media query that any framework can detect; Chrome DevTools Performance panel measures dropped frames framework-agnostically

## Related skills
- `rendering-performance` — animation jank often manifests as INP regression
- `accessibility-audit` — reduced-motion respect is a WCAG requirement

## Reference
- **Key insight encoded**: Wrap the app in `<MotionConfig reducedMotion="user">` for global opt-out — single source of truth. Constrain animated props to `transform`/`opacity` so animations stay on the GPU (and don't shift layout → CLS). Prefer `layout` over manual position animation for free FLIP-style transitions. Motion-specific WCAG lives in *this* skill, not just reduced-motion: nothing flashes >3×/s (2.3.1), and auto-playing motion >5s must be pausable (2.2.2). Tune the feel — fast durations (100–300ms) with ease-out entrances / ease-in exits.

