# Animation Accessibility

> Design and verify reduced-motion behavior for web interfaces. Use when adding or reviewing `prefers-reduced-motion`, replacing spatial motion with a safer state change, handling autoplaying or looping media, smooth scrolling, or configuring framework hooks such as `useReducedMotion` and `MotionConfig`.

- Skill: `somtougeh/animation-accessibility` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add somtougeh/animation-accessibility`
- Raw SKILL.md: https://api.skillmd.com/api/skills/somtougeh/animation-accessibility/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: SomtoUgeh (https://skillmd.com/u/somtougeh)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/somtougeh/animation-accessibility

---


# Accessible Animation

Animations are used to strategically improve an experience. **To some people, they degrade it.** Motion can [make people feel sick](https://www.a11yproject.com/posts/understanding-vestibular-disorders/) — vestibular disorders are real and common — or simply pull attention away from the task. That's not the experience you're building.

Distilled from Emil Kowalski's *Animations on the Web* course ([animations.dev](https://animations.dev/)). This skill covers **reduced motion**: reading the user's preference and deciding what each animation becomes when it's set.

Follow [../animate/references/canonical-policy.md](../animate/references/canonical-policy.md)
for the shared motion policy. It wins if this guide conflicts with it.

## The preference

Most devices let users state a preference for animation, and browsers expose it through the `prefers-reduced-motion` media query:

- **`no-preference`** — the user has not set a preference. No change needed.
- **`reduce`** — the user has set a preference. Your animation must be altered.

Because the preference exists at the OS level, a user who set it once expects *every* site to honor it. There is no per-site opt-in to wait for.

## Preserve meaning and reduce motion

`reduce` can mean an instant change, a non-spatial transition, or no decorative
animation. Choose the least motion that preserves the interaction's meaning.

The transformation is: **remove the motion, keep the meaning.**

| Under `reduce` | Do |
| --- | --- |
| Movement — `transform`, `translate`, `scale`, position, layout | **Remove.** Nothing should move. |
| Meaning — `opacity`, `color`, `background-color` | **Keep.** These carry the state change without motion. |
| Autoplaying and looping animation | **Disable** — or pause it on a representative frame. |
| Purely decorative motion (an idle float, an ambient loop) | **Remove entirely.** It conveys nothing, and lingering motion falsely implies interactivity. |

So a modal that scales in becomes a modal that fades in. A sidebar that slides from `-100%` becomes a sidebar that fades. A multi-step form that slides horizontally between steps crossfades instead. The state change is still legible; nothing travels across the screen.

## Workflow

Design both preference states from the start:

1. **Design both preference states.** Decide what information the motion carries.
2. **Implement `prefers-reduced-motion`.** Apply the table above. Test it with the preference on — [Chrome DevTools can emulate it](https://developer.chrome.com/docs/devtools/rendering/emulate-css#emulate_css_media_feature_prefers-reduced-motion) (Rendering panel → *Emulate CSS media feature prefers-reduced-motion*).
3. **Ship two variants.** One for `no-preference`, one for `reduce`.

You're done when every animation you touched has both variants and you have watched the `reduce` variant with emulation on. Reasoning about it is not the same as seeing it — the common failure is a "reduced" variant that still moves, because one `transform` was left behind in a shared class or a spring config.

## Implementation

### CSS

Replace spatial motion when meaning benefits from a transition; otherwise disable
the animation:

```css
.element {
  animation: bounce 0.2s;
}

@media (prefers-reduced-motion: reduce) {
  .element {
    animation: fade 0.2s;
  }
}
```

### Tailwind

`motion-safe:` and `motion-reduce:` variants map to the two media queries:

```html
<svg class="motion-safe:animate-bounce motion-reduce:animate-none" viewBox="0 0 24 24">
  <!-- ... -->
</svg>
```

### Framer Motion — `useReducedMotion`

The hook returns `true` when the user prefers reduced motion, so you can branch individual values:

```jsx
import { useReducedMotion, motion } from "motion/react";

export function Sidebar({ isOpen }) {
  const shouldReduceMotion = useReducedMotion();
  const closedX = shouldReduceMotion ? 0 : "-100%";

  return <motion.div inert={!isOpen} aria-hidden={!isOpen} animate={{ opacity: isOpen ? 1 : 0, x: isOpen ? 0 : closedX }} />;
}
```

Motion 13.2's built-in hook reads the preference on mount. For live preference changes, use the external-store hook and worked example in [SNIPPETS.md](SNIPPETS.md). A preference hook can also branch whole **variant sets** and gate properties CSS can't reach: skip height animation and pass `layout={false}` under `reduce`, since layout animations move things by definition.

### Framer Motion — `MotionConfig` (app-wide safety net)

`reducedMotion="user"` makes Framer Motion respect the preference everywhere below it, disabling transform and layout animations while preserving other values, such as `opacity` and `backgroundColor`. It does not disable every possible spatial effect (for example, explicit height animation needs its own handling):

```jsx
import { MotionConfig } from "motion/react";

<MotionConfig reducedMotion="user">{children}</MotionConfig>
```

**The default is `never`, so this does nothing until you set it.** Wrap the application as a baseline, then check every component override, explicit dimension animation, SVG effect, and decorative loop with `useReducedMotion` where needed.

## Visuals: jump, don't tween

Anything that reads as an image or video — a visual metaphor explaining a concept — often *can't* be removed, because the motion is what carries the explanation.

Reduce it instead of deleting it: **jump between the frames rather than animating between them.** The user still gets every state of the sequence; nothing slides or morphs on screen.

## Snippets

[SNIPPETS.md](SNIPPETS.md) has copy-ready recipes for the cases with a specific mechanism:

- Smooth scrolling — enable only under `no-preference`
- Autoplaying images (GIF / animated AVIF) — static fallback via `<picture>`
- Autoplaying video — paused with visible controls under `reduce`
- Looping animation — pause it on a **hero frame** rather than frame 0
- Dependency-free `useReducedMotion` hook
- Worked example: reduced-motion variants for a multi-step component

---

For the surrounding decisions — whether to animate at all, easing, duration, springs — see the `animate` skill. For frame budget and GPU concerns, see `animation-performance`.

