Guide

Accessible animation: a developer's WCAG guide

Accessible animation is motion that adds meaning without excluding anyone — it can be paused, it will not trigger a seizure, and it steps aside for people who have asked their device for less movement. Most of the guidance written about it predates WCAG 2.2 and stops at a single reduced-motion snippet. This guide is current to 2026 and works the way an audit does: criterion by criterion. Three success criteria govern web motion; below is what each one requires, the pattern that satisfies it, and copy-paste code that passes. If you build motion into UIs — or ship it from generated code — this is the checklist to build against.

Why animation is an accessibility concern

Motion is not neutral. For people with vestibular disorders, large parallax scrolls, zoom transitions, and spinning elements can cause genuine dizziness, nausea, and migraines. Flashing content can trigger photosensitive seizures. Continuous, auto-playing movement pulls focus away from anyone with attention or cognitive differences trying to read the page underneath it. None of these are edge cases; each affects a real slice of every audience. The web platform gives you the signals and the controls to handle all three — the job is to use them deliberately rather than hope a component library did.

The three WCAG criteria that govern motion

Accessibility guidelines for animation are not a vague "be considerate." They resolve to three specific, testable success criteria in WCAG 2.2. Meet these and the rest of accessible animation is refinement:

  • 2.2.2 Pause, Stop, Hide (Level A) — auto-starting motion that lasts more than five seconds needs a control to pause, stop, or hide it.
  • 2.3.1 Three Flashes or Below Threshold (Level A) — nothing may flash more than three times in any one second.
  • 2.3.3 Animation from Interactions (Level AAA) — motion triggered by a user action must be disableable, unless the motion is essential.

The two Level-A criteria are the floor almost every accessibility policy points at; 2.3.3 is AAA but is easy to meet with one media query and is where the well-known prefers-reduced-motion pattern lives. Take them one at a time.

WCAG 2.2.2 — Pause, Stop, Hide

If motion starts on its own, runs longer than five seconds, and sits alongside other content, the user must be able to stop it. Carousels, auto-scrolling tickers, animated backgrounds, and looping hero videos are the usual offenders. A five-second-or-shorter animation that plays once is fine; a loop is not, because a loop never ends on its own.

The mechanism can be as small as a button that toggles animation-play-state. Drive it from a data attribute so the control and the CSS stay in sync:

<div class="ticker" data-playing="true">…</div>
<button type="button" class="ticker-toggle" aria-pressed="true">Pause</button>
.ticker[data-playing="false"] { animation-play-state: paused; }
const ticker = document.querySelector('.ticker');
const toggle = document.querySelector('.ticker-toggle');

toggle.addEventListener('click', () => {
  const playing = ticker.dataset.playing === 'true';
  ticker.dataset.playing = String(!playing);        // flips the CSS
  toggle.setAttribute('aria-pressed', String(!playing));
  toggle.textContent = playing ? 'Play' : 'Pause';
});

The aria-pressed state is what makes the control legible to assistive technology — a bare icon with no state announced is only half a fix. And the calmest way to pass 2.2.2 is often to not auto-play at all: start the ticker paused and let the person choose to run it.

WCAG 2.3.1 — Three Flashes or Below Threshold

This one protects against photosensitive seizures, and it is the least negotiable criterion in the set: nothing on the page may flash more than three times in any one-second window (unless the flashing area is below the general and red-flash size thresholds). Rapid strobing color changes, a hard flicker between high-contrast frames, or a stuttering GIF can all cross the line.

There is no clever CSS that makes fast flashing safe — the fix is to not do it. In practice that means: keep any repeated flash to three per second or fewer, avoid large areas of saturated red in rapid transitions, and prefer a smooth cross-fade over a hard cut when something must draw the eye. If you have inherited content you are unsure about, tools like the Photosensitive Epilepsy Analysis Tool (PEAT) analyze a recording frame by frame. When in doubt, slow it down; a gentle transition reads as more polished anyway.

WCAG 2.3.3 — Animation from Interactions

Motion set off by a user action — a scroll-triggered reveal, a hover that zooms, a parallax layer that moves as you scroll — must be able to be switched off, unless it conveys essential information. The switch you use is the platform's own signal: prefers-reduced-motion. Build the interaction motion as an opt-in, so "reduced" is the default and you only add movement when the person has not asked against it:

/* Default: the element is simply present, no motion. */
.reveal { opacity: 1; }

/* Motion is layered on only when it is welcome. */
@media (prefers-reduced-motion: no-preference) {
  .reveal { animation: rise 0.5s ease-out both; }
}

JavaScript-driven motion has to check the same preference itself — a GSAP timeline or scroll library does not read your CSS media query. Gate it with matchMedia, and listen for changes so the page reacts if someone flips the setting live:

const reduce = window.matchMedia('(prefers-reduced-motion: reduce)');

function setupMotion() {
  if (reduce.matches) return;   // honor the preference: skip the effect
  // …start the scroll/hover animation here…
}

setupMotion();
reduce.addEventListener('change', setupMotion);

That is the whole of 2.3.3 for most sites. The full patterns — the blanket opt-out, the per-library gotchas, and the four ways to confirm the guard actually fires — are in the companion guide, prefers-reduced-motion, explained (and how to test it).

Beyond the letter: essential vs. decorative, and performance

Passing the three criteria is the floor, not the ceiling. Two judgment calls decide whether animation is genuinely accessible:

  • Essential vs. decorative. "Reduce" means less motion, not a frozen page. A decorative slide-in should disappear under reduced-motion; a progress indicator or a transition that shows what just changed carries meaning and may remain, if it is truly essential. When you strip motion, swap large movement for a gentle opacity fade rather than removing all feedback — a state change nobody can perceive is its own barrier.
  • Performance is accessibility. Animation that drops frames or shifts layout is a barrier for people on low-end devices and anyone with motion sensitivity, who feel jank as unease. Animate only transform and opacity (the compositor-friendly properties), never width, top, or box-shadow in a loop. Smooth, cheap motion is kinder motion.

An accessible-animation checklist

  • Auto-playing motion over five seconds has a visible pause/stop control with an announced state (2.2.2).
  • Nothing flashes more than three times per second (2.3.1).
  • Interaction-triggered motion is guarded by prefers-reduced-motion in both CSS and JS (2.3.3).
  • Reduced-motion degrades to a calm fade, not a dead page; essential motion is a deliberate exception.
  • Animations move only transform/opacity and hold a steady frame rate.
  • You have tested it — with the OS setting on, not just in code.

How to verify a page

A checklist is only as good as the audit behind it. For the manual, tool-independent walkthrough — inventory every motion source, then check each criterion by hand — see How to verify web motion accessibility. To scan a whole page's CSS in one pass, paste the URL into the free motion accessibility check: it statically reads the linked CSS and <style> blocks against WCAG 2.2.2 and 2.3.3, resolves whether each animation is actually guarded, and returns every finding with the fix — no signup, nothing stored. It is deliberately honest about its scope: runtime JS/GSAP motion is reported as not audited rather than silently passed, so pair the scan with the OS-toggle test for full coverage.

The durable answer is to stop relying on every developer and every generated snippet to remember the guard, and make it structural instead — the layer MotionSpec builds: animation assembled from a reviewed catalog of primitives that each ship a prefers-reduced-motion fallback and a performance budget by construction, so inaccessible motion cannot appear in the first place. For how unguarded motion gets into a codebase to begin with, see What is motion slop? and How to detect and fix motion slop.

Check your animation against WCAG in ten seconds.

Paste any URL into the free motion accessibility check and see which animations pass 2.2.2 and 2.3.3 — every finding with the fix.

Run the free motion check