skip to content

prefers-reduced-motion and Accessible Motion

Honoring the OS-level motion preference is both an accessibility requirement and the most common follow-up to any animation question. Covers how to scope motion behind the query without maintaining two parallel stylesheets.

part ofCSSoverview, primer and where to startread it →
on this pageshow

questions

5

In CSS, what is the difference between putting motion behind @media (prefers-reduced-motion: no-preference) and declaring it normally then stripping it inside @media (prefers-reduced-motion: reduce), and what does each pattern get wrong?

level: middleimportance: must knowfreq 55%

answer

  1. two patterns, opposite defaults
  2. what happens when you forget a rule
  3. an unknown media feature never matches
  4. no-preference block is the enhancement
  5. media queries add no specificity

basics

~20 s

Opt-in puts animation inside a no-preference query, so anything you forget simply never moves. Opt-out ships motion to everyone and removes it in a reduce query, so any animation you forget to undo still plays for people who asked for less.

solid answer

~50 s

Both read the same OS-level switch, but they differ in what the *default* is. The opt-out pattern declares `transition`/`animation` normally and then cancels it inside `@media (prefers-reduced-motion: reduce)`; the failure mode is that the reset is a subtraction list you must keep in sync, so every new animation is motion-on until someone remembers it. The opt-in pattern declares motion only inside `@media (prefers-reduced-motion: no-preference)`, making motion a progressive enhancement — forgetting a rule fails safe to stillness. Its cost is that every animated component needs a query block, and because an unrecognised media feature never matches, a browser without support gets no motion at all. In practice teams use both: a global opt-out reset as the floor, opt-in on new components. Remember the media query adds no specificity, so an opt-out reset that sits earlier in the sheet loses to the base rule.

code

css · 19 lines
css
/* Opt-out: everyone gets motion, reduce-motion users have it removed. */
.card {
  transition: transform 200ms ease;
}
@media (prefers-reduced-motion: reduce) {
  .card {
    transition: none;
  }
}

/* Opt-in: nothing moves until the user's environment reports no-preference. */
.panel {
  border: 1px solid currentColor;
}
@media (prefers-reduced-motion: no-preference) {
  .panel {
    transition: transform 200ms ease;
  }
}

go deeper

for a junior

Know that the query exists and that it reads an operating-system setting. Be able to write both blocks correctly and say which one leaves motion on by default.

for a middle

Explain the two authoring patterns as a choice of default, and name each one's failure mode: an unmaintained subtraction list versus a query block around every animated component. Mention that the media query adds no specificity.

for a senior

Show how you would roll this out across an existing codebase — a global floor plus opt-in for new work, plus where third-party CSS and script-written inline styles escape the reset — and how you would keep the rule from silently regressing.

for a principal

Own the tradeoff between a single centrally gated motion token that every component consumes and per-component query blocks that stay local and readable. Be ready to argue which is enforceable at your team's size and how you would detect drift.

## The signal both patterns read `prefers-reduced-motion` is a CSS media feature that reports exactly one thing: whether the user has asked the operating system to cut down non-essential motion. macOS and iOS expose it as "Reduce motion", Windows as turning off animation effects, Android as "Remove animations", GNOME as disabling animations. The browser maps that switch onto two values, `reduce` and `no-preference`. A page can read it but never set it, and there is no in-between value — the granularity question ("how much less?") is left to the author. There are two ways to react to that signal, and they are not stylistic twins: they choose opposite defaults. ```css /* opt-out: motion is the default, removed on request */ .card { transition: transform 200ms ease; } @media (prefers-reduced-motion: reduce) { .card { transition: none; } } /* opt-in: stillness is the default, motion is the enhancement */ @media (prefers-reduced-motion: no-preference) { .card { transition: transform 200ms ease; } } ``` ## Opt-out: a subtraction list you must maintain The opt-out pattern is what you retrofit onto an existing codebase, and its weakness is organisational rather than technical. The `reduce` block is a list of things to undo, and it only stays correct if every future author remembers to extend it. The moment someone adds `animation: pulse 1s infinite` to a new component and does not touch the reset, a user who explicitly asked for less motion gets a pulsing element. Third-party stylesheets, and any styles a script writes inline, are outside the reset entirely — an inline `style` attribute cannot be overridden by an author rule without `!important`. Opt-out also runs into the cascade. A media query contributes nothing to specificity: `@media (prefers-reduced-motion: reduce) { .card { transition: none } }` has exactly the specificity of `.card`. It beats the base rule only because it comes later in the same origin and layer. Move the query block to the top of the file, or leave the base rule in a later `@layer`, and the reset silently stops working while still looking correct in review. This is why the widely-copied global reset reaches for `!important` on a universal selector — and why that hammer then also flattens per-component overrides you actually wanted. ## Opt-in: fails safe, at the cost of ceremony The opt-in pattern inverts the default. Nothing animates unless the user's environment has affirmatively reported `no-preference`, which turns motion into a progressive enhancement in the strict sense: the interface is complete and usable with the query stripped out. A forgotten rule becomes a missing flourish rather than an accessibility defect, which is the right way for a mistake to fail. The costs are real but small. Every animated component needs its declarations inside a query block, which duplicates selectors when the same rule also carries non-motion declarations, and it fights against grouping styles by component rather than by condition. The subtler cost is unsupported environments. Media Queries Level 4 gives unknown media features a third state — the query evaluates to unknown and therefore does not match, and negating it does not rescue you, so neither `(prefers-reduced-motion: no-preference)` nor `not (prefers-reduced-motion: reduce)` matches on an engine that has never heard of the feature. With opt-in, such a browser gets a completely static interface. Every current major engine supports the feature, so this argument has largely expired, but it is the historical reason opt-out was recommended first and it is still the correct answer to "what happens on a browser that doesn't know the feature". ## The hybrid teams actually ship A global reset gives you a floor that covers third-party CSS and anything you missed; opt-in on individual components gives you a correct default going forward. A third variant routes both through a custom property so a component never writes a raw duration at all — the gate lives in one place and components consume `var(--motion-duration)`. ```css :root { --motion-duration: 200ms; } @media (prefers-reduced-motion: reduce) { :root { --motion-duration: 0.01ms; } } .card { transition: transform var(--motion-duration) ease; } ``` ## What "handled" means Whichever pattern you pick, honouring the preference is not the same as deleting the animation. `reduce` asks you to cut motion that moves large amounts of content across the screen; a short opacity crossfade or an instant state change usually preserves the feedback the animation was carrying. And CSS is only part of the surface: `scroll-behavior: smooth`, and any animation driven from script, need the same gate applied at their own layer. ## Checklist - Decide the default deliberately; do not let it be an accident of which file was edited last. - Keep the reset after the rules it overrides, or in a layer that wins. - Remember the query adds no specificity. - Treat `reduce` as "substitute", not always "delete".

  • Your opt-out reset is written at the top of the stylesheet and the animation still plays. Why?
    A media query contributes nothing to specificity. The rule inside `@media (prefers-reduced-motion: reduce)` has the same specificity as the base rule, so with the query first, the later base rule wins on source order. Move the query after the rules it cancels, put it in a cascade layer that wins, or route the value through a custom property the base rule already reads.
  • On a browser that doesn't implement prefers-reduced-motion, which pattern degrades to motion and which to stillness?
    An unrecognised media feature makes the query fail to match, and negation does not help. So opt-in degrades to stillness — the `no-preference` block never applies — while opt-out degrades to full motion, because the base rules apply and the `reduce` block never does. Opt-out was recommended historically for exactly that reason.
  • Does honouring the preference mean you must set animation: none?
    No. The preference asks for reduced motion, not for a static page. Replacing a large translate or zoom with a short opacity crossfade, or applying the end state instantly, usually keeps the feedback while removing the vestibular trigger. Removing the transition entirely is a valid choice, but it is a choice, not the definition of compliance.

Opt-out is a deny-list you have to keep updating; opt-in is an allow-list where anything you forget stays safely switched off.

saying these in an interview costs you the question

  • Thinks the media query itself raises specificity of the reset
  • Assumes a forgotten animation is harmless under the opt-out pattern
  • Claims not (prefers-reduced-motion: reduce) matches on unsupporting browsers
  • Says honouring the preference always means animation: none
  • Believes the preference is something CSS or JS can set for the user

context

open as a page

In CSS, where does the prefers-reduced-motion media feature get its value from, what are its two possible values, and what does the bare query @media (prefers-reduced-motion) match?

level: juniorimportance: should knowfreq 45%

basics

~20 s

The browser reads an operating-system accessibility setting and exposes it to CSS as one of two values: reduce when the user asked for less motion, and no-preference otherwise. The bare query without a value matches only reduce, because no-preference evaluates as false in boolean context.

open as a page

When @media (prefers-reduced-motion: reduce) matches, which motion in an interface should actually be removed or substituted, and which motion is reasonable to keep?

level: middleimportance: should knowfreq 40%

basics

~20 s

Remove large-area movement — long translations, zooms, parallax, spinning or bouncing content — because that is what triggers vestibular symptoms. Keep small, localised, essential motion such as a progress indicator, and substitute a short opacity crossfade or an instant state change where the animation was carrying feedback.

open as a page

A stylesheet handles reduced motion with a universal rule that sets animation-duration and transition-duration to 0.01ms !important instead of using animation: none. Why the near-zero duration rather than none, and what does that blanket reset still get wrong?

level: seniorimportance: should knowfreq 38%

basics

~20 s

A near-zero duration still runs the animation, so animationend and transitionend fire and script that waits on them keeps working; animation: none suppresses them and can leave such code stalled forever. The reset is a safety floor, not a design: it flattens motion instead of substituting something gentler.

open as a page

You own a design system whose components animate. How do you make reduced-motion support a structural guarantee rather than per-component discipline, and how do you let a user override the operating-system preference from inside the app?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Route every duration through a small set of motion custom properties gated once at the root, so components never write a raw duration and cannot forget the query. Layer an app-level override on top by keying the same properties off a root attribute, which outranks the media query on specificity.

open as a page