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?
answer
- two patterns, opposite defaults
- what happens when you forget a rule
- an unknown media feature never matches
- no-preference block is the enhancement
- media queries add no specificity
basics
~20 sOpt-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 sBoth 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/* 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
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.
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.
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.
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