Guide

prefers-reduced-motion, explained (and how to test it)

prefers-reduced-motion is the CSS media feature that tells your site whether the person using it has asked their device for less animation. Honoring it is the single most important thing you can do for motion accessibility — and it is easy to implement and, it turns out, easy to get subtly wrong. This guide covers what the media query is, the CSS and JavaScript patterns that actually pass, the four ways to test prefers-reduced-motion on a real page, and how it maps to WCAG. Every code sample here is copy-paste ready.

What prefers-reduced-motion is

Modern operating systems expose a "reduce motion" setting. When someone turns it on — often because animation makes them physically unwell — the browser reports it to CSS through the prefers-reduced-motion media feature. It has two values: reduce (the person has asked for less motion) and no-preference (they have not). Your job is to design for both.

This matters because motion is not neutral. For people with vestibular disorders, large parallax scrolls, zooming transitions, and spinning elements can trigger genuine dizziness, nausea, and migraines. Reduced motion accessibility is not a nice-to-have polish item; for a real slice of your users it is the difference between a usable page and one they have to close.

The prefers-reduced-motion CSS media query: syntax and values

There are two ways to write the guard. The first is the blanket opt-out: let motion run by default, then tone it down when reduce is requested.

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

This is a reasonable safety net for a codebase you are retrofitting, but it is blunt: it kills all motion. The better pattern for new work is the opt-in, where motion only runs when the person has not asked for less:

/* Default: no motion. Motion is added only on request. */
.card { opacity: 1; }

@media (prefers-reduced-motion: no-preference) {
  .card { animation: slide-in 0.4s ease-out; }
}

With the opt-in pattern, "reduce" is the default state, so someone who wants less motion gets the calm version without your CSS having to undo anything. It is progressive enhancement applied to movement.

Reading it in JavaScript with matchMedia

JavaScript-driven motion — a WAAPI animation, a GSAP timeline, a scroll library — needs to check the preference itself. Use window.matchMedia, and listen for changes so the page responds if the user flips the setting without reloading:

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

function applyMotionPreference() {
  if (query.matches) {
    // The user wants less motion: skip or shorten JS animations.
  } else {
    // Safe to run the richer motion.
  }
}

applyMotionPreference();
query.addEventListener('change', applyMotionPreference);

The change listener is the part people forget. Someone may turn reduce motion on because your page made them queasy — reacting live costs two lines.

How to test prefers-reduced-motion

Implementing the guard is half the work; the other half is confirming it fires. Four ways, from most manual to most automatic:

  1. Toggle the OS setting — the ground truth.
    • macOS: System Settings → Accessibility → Display → Reduce motion.
    • Windows: Settings → Accessibility → Visual effects → Animation effects off.
    • iOS: Settings → Accessibility → Motion → Reduce Motion. Android: Settings → Accessibility → Remove animations.
  2. Emulate it in DevTools — in Chrome/Edge, open the Command Menu (Cmd/Ctrl + Shift + P), run "Emulate CSS prefers-reduced-motion", and reload. Fast while iterating, but it emulates the CSS signal, not the OS.
  3. Assert it in code — read the preference the same way your app does:
    console.log(window.matchMedia('(prefers-reduced-motion: reduce)').matches);
    In an e2e test, launch the page with the preference forced on, then assert the animated element has no active animation. That turns "we honor reduced motion" into a test that fails loudly.
  4. Scan the whole page at once — the three methods above test one page, by hand. To check every animation in a page's CSS in one pass, paste the URL into the free motion accessibility check. It statically scans 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.

Common mistakes

  • Leaving motion unguarded — animation with no prefers-reduced-motion block at all. If grep -rn "prefers-reduced-motion" src returns nothing, this is you.
  • Nuking every animation — "reduce" means less motion, not a frozen page. Swap large movement for a gentle opacity fade rather than removing all feedback.
  • Ignoring essential motion — a genuine progress indicator may keep running under reduce, if it is truly essential. Decoration does not get that pass.
  • Only guarding CSS — if your JS/GSAP/scroll library animates without checking matchMedia, the CSS guard is irrelevant to that motion.

How it maps to WCAG

prefers-reduced-motion is the practical mechanism behind WCAG 2.3.3 Animation from Interactions (Level AAA): motion triggered by interaction should be disableable unless essential. It also supports WCAG 2.2.2 Pause, Stop, Hide (Level A). The media query is not the whole of motion accessibility, but it is the load-bearing piece.

FAQ

What does prefers-reduced-motion do?

It is a CSS media feature that reports whether someone has turned on their device's "reduce motion" setting. It has two values — reduce and no-preference — so your CSS and JavaScript can serve a calmer version to people who ask for less animation.

How do I test prefers-reduced-motion?

Four ways: toggle the OS setting (the ground truth), emulate it in Chrome or Edge DevTools, assert window.matchMedia('(prefers-reduced-motion: reduce)').matches in an e2e test, or scan a whole page's CSS at once with the free motion check.

Does reduced motion mean removing all animation?

No. "Reduce" means less motion, not a frozen page. Swap large movement — parallax, zooms, sweeping transitions — for a gentle opacity fade, and let genuinely essential motion like a real progress indicator keep running.

Does the CSS guard cover JavaScript animation?

No. A @media (prefers-reduced-motion) block only governs CSS. WAAPI, GSAP, and scroll libraries have to check window.matchMedia themselves, or they will animate straight through the user's preference.

Check prefers-reduced-motion on your own site.

Paste any URL into the free motion check — a static scan against WCAG 2.2.2 and 2.3.3, every finding with its fix. No signup, nothing stored.

Run the free motion check

Generating web UI at scale? See the Design Partner Program.