TechEarl

prefers-reduced-motion: Respecting Users Who Get Motion Sick

The prefers-reduced-motion media query reads the OS reduce-motion setting so you can tone down animation for users who get motion sick. The two patterns, the safe global reset, the matchMedia() check, and pairing it with scroll-behavior.

Ishan Karunaratne⏱️ 8 min readUpdated
Share thisCopied
Use the prefers-reduced-motion media query to respect the OS reduce-motion setting: opt-out and opt-in patterns, a safe global reset, the matchMedia check, and scroll-behavior.

If you want to respect users who get motion sick, wrap your animations in a media query and tone them down when the operating system asks you to:

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

That block is the whole idea in one paragraph. prefers-reduced-motion is a CSS media feature that reflects an operating-system accessibility setting: when a user turns on "reduce motion" in their OS, the browser reports reduce, and your stylesheet can collapse or remove animation for them. It is Baseline, supported in every current browser, and has been safe to ship for years. The rest of this page is the detail behind that block: why it matters, the two ways to wire it up, and the JavaScript equivalent for animation you cannot express in CSS alone.

Why this matters (it is not a nice-to-have)

For a lot of people, movement on a screen is not a delight, it is a problem. Users with vestibular disorders, and plenty of people without a diagnosis, get genuinely sick from parallax scrolling, large zooming transitions, spinning loaders, and content that slides across the viewport. The symptoms are real: nausea, dizziness, headaches, disorientation. The same animation you tuned for an hour to feel "premium" can make someone close the tab and lie down.

Operating systems give those users a single switch. macOS calls it Reduce Motion (System Settings → Accessibility → Display). Windows calls it Animation effects off (Settings → Accessibility → Visual effects). iOS, Android, and the major Linux desktops all have an equivalent. prefers-reduced-motion is how that one OS-level switch reaches your CSS, so the user sets their preference once and every site that bothers to listen honors it.

This also maps to WCAG 2.3.3 Animation from Interactions, a Level AAA success criterion: motion triggered by interaction (scroll-driven parallax, click-triggered transitions) should be disable-able unless it is essential. Honoring prefers-reduced-motion is the standard way to meet it.

The two values, and the two patterns

The feature has exactly two values: no-preference (the user has not asked for reduced motion) and reduce (they have). That gives you two ways to write your CSS, and the difference matters.

Pattern 1: opt-out (animate by default, kill it inside the query). This is the most common shape. You write your animations normally, then add one block that removes them for users who asked:

css
.card {
  transition: transform 0.3s ease;
}
.card:hover {
  transform: translateY(-4px);
}

@media (prefers-reduced-motion: reduce) {
  .card {
    transition: none;
  }
}

Pattern 2: opt-in (no animation by default, add it only for no-preference). Here you ship the static experience to everyone, and treat motion as an enhancement that only the unaffected majority opts into:

css
.card:hover {
  transform: translateY(-4px);
}

@media (prefers-reduced-motion: no-preference) {
  .card {
    transition: transform 0.3s ease;
  }
}

The opt-in pattern is the safer default for new work, because the resting state of your CSS is the accessible one. If a browser ever fails to report the preference, or a user has not discovered the OS setting yet, they still get a calm page. Use opt-out when you are retrofitting motion into an existing site and a full inversion would be too invasive.

The global safe reset (and the one subtlety in it)

Auditing every individual animation is tedious, so most projects also drop a single catch-all reset near the top of their stylesheet:

css
@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;
  }
}

Notice the duration is 0.01ms, not 0, and the animation is not removed with animation: none. That is deliberate, and it is the one subtlety people get wrong. A lot of animations apply their final visual state only when the animation runs (a fade-in that starts at opacity: 0, an element that slides in from off-screen). If you hard-kill the animation with animation: none, those elements can get stuck in their start state: invisible, or parked off-screen. Crushing the duration to a near-zero value instead lets the animation run to its end-state instantly, so nothing motion-sickening happens but nothing breaks either. Treat the reset as a backstop, not a replacement for thinking about each animation.

Reading the preference in JavaScript

CSS handles most of this, but some motion lives in JavaScript: a canvas loop, a scroll-triggered library, a scrollIntoView call, a third-party animation. Read the same preference with matchMedia():

javascript
const teReduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)");

function teApplyMotion() {
  if (teReduceMotion.matches) {
    // user wants reduced motion: skip the animation, jump to the end state
    target.scrollIntoView();
  } else {
    target.scrollIntoView({ behavior: "smooth" });
  }
}

teApplyMotion();
teReduceMotion.addEventListener("change", teApplyMotion);

Two things worth keeping. First, .matches is the current boolean answer. Second, the change listener fires if the user flips the OS setting while your page is open, so the page can respond without a reload. If you start a requestAnimationFrame loop, an autoplaying carousel, or anything that moves on its own, gate it behind teReduceMotion.matches the same way.

Pair it with scroll-behavior

The reset above already includes scroll-behavior: auto !important for a reason. If you have opted into smooth scrolling sitewide with scroll-behavior: smooth, that is animation too, and a long smooth scroll across a tall page is exactly the kind of large-viewport movement that triggers vestibular symptoms. So scope smooth scrolling to people who have not asked for less motion:

css
@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

That is the opt-in pattern again, applied to scrolling. For the full treatment of the property, including the JavaScript scrollIntoView({ behavior }) option and its gotchas, see CSS smooth scroll. And if your animations have JavaScript that needs to run after they finish, the transitionend and animationend events still fire even at the near-zero duration this reset uses, which is another reason to crush the duration rather than remove the animation outright.

FAQ

See also

Sources

Authoritative references this article was fact-checked against.

Tagsprefers-reduced-motionCSSaccessibilityanimationa11ymatchMediascroll-behaviorWCAG

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts