What Does prefers-reduced-motion Do? CSS, JS and How to Test
How the prefers-reduced-motion: reduce media query works, what to tone down instead of remove, how to read it in JavaScript, and how to test it.
prefers-reduced-motion: reduce is a CSS media feature that matches when the user has turned on a "reduce motion" setting on their device. You wrap your toned-down styles in @media (prefers-reduced-motion: reduce) { ... }, and the browser applies them only for people who asked for less movement.
The part people get wrong is what goes inside that block. The setting asks you to remove, reduce, or replace motion-based animation, not to strip every transition and leave the interface feeling broken. This article covers the syntax, what to swap rather than delete, how to read the preference in JavaScript, how to turn it on and off, and how to test it.
What the prefers-reduced-motion media query does#
The feature detects whether a user has enabled a setting to minimize non-essential motion. The reason it exists is vestibular motion disorders. Animations such as scaling or panning large objects can be vestibular motion triggers, and they can cause real discomfort. It's a well-established feature, available across browsers since January 2020 (MDN reference).
There are two values:
| Value | Meaning |
|---|---|
no-preference |
The user hasn't made a preference known. Evaluates as false. |
reduce |
The user enabled reduced motion on their device. Evaluates as true. |
Because no-preference evaluates as false, @media (prefers-reduced-motion) with no value is equivalent to @media (prefers-reduced-motion: reduce). I'd still write : reduce every time. It reads clearly and nobody has to remember the shorthand.
Reduce instead of remove#
The setting conveys that the user prefers an interface that removes, reduces, or replaces motion. Those are three different tools, and "remove" is only one of them.
My rule of thumb, as advice rather than a spec: look at what kind of motion you have.
- Large-scale movement (scaling, panning, parallax, big slides): replace it. A fade communicates the same state change without the vestibular trigger.
- Looping decoration (pulsing badges, endless spinners that aren't communicating progress): remove or slow down.
- Feedback that carries meaning (a button press, a form error appearing, a toast showing up): keep a change, but make it gentle. An opacity or color change still tells the user something happened.
The MDN example follows exactly this idea. It swaps a scaling pulse for an opacity dissolve, which the page describes as a more muted animation that is not a vestibular motion trigger.
A prefers-reduced-motion example you can copy#
This is the smallest useful pattern, adapted from the MDN example. The default rule animates with scale. The reduce rule has the same specificity and comes later in source order, so it wins when the preference is on.
.animation {
animation: pulse 1s linear infinite both;
background-color: purple;
}
/* Tone down the animation to avoid vestibular motion triggers. */
@media (prefers-reduced-motion: reduce) {
.animation {
animation: dissolve 4s linear infinite both;
background-color: green;
text-decoration: overline;
}
}
@keyframes pulse {
0% { transform: scale(1); }
25% { transform: scale(0.9); }
50% { transform: scale(1); }
75% { transform: scale(1.1); }
100% { transform: scale(1); }
}
@keyframes dissolve {
0% { opacity: 1; }
50% { opacity: 0.3; }
100% { opacity: 1; }
}
In the original, the green background and the overline exist only to make the switch visible in a demo. In real code you'd drop them and keep just the animation swap.
Notice the order. If you put the media query block before the base rule, the base rule overrides it and nothing changes. That's the most common reason "it doesn't work".
Variation: opt in to motion instead#
You can flip the logic and only add animation when the user hasn't asked for less. This uses no-preference:
.card {
opacity: 1;
}
@media (prefers-reduced-motion: no-preference) {
.card {
animation: pulse 1s linear infinite both;
}
}
I like this for purely decorative motion. The safe state is the default, and motion is the extra. The tradeoff is that a browser or device that doesn't report a preference gets the animation, since nothing says otherwise, so decide per case. For motion that carries meaning, the "reduce" override from the previous example is usually the better fit, because you're replacing the motion rather than deleting it.
prefers reduced motion in JavaScript#
CSS covers most cases, but some motion lives in script: scroll-driven effects, canvas, carousels that auto-advance. Read the same media query with window.matchMedia:
const query = window.matchMedia('(prefers-reduced-motion: reduce)');
function apply(reduce) {
document.documentElement.classList.toggle('reduce-motion', reduce);
}
apply(query.matches);
query.addEventListener('change', (event) => apply(event.matches));
query.matches is true when the user has reduce on. The change listener matters because people can flip the setting while your page is open. See MDN's matchMedia docs for the full API.
Here's a small Node script that shows the decision logic I'd use for choosing a replacement. It classifies animated properties and picks a reduced version. It's a toy, but it makes the "replace, don't just delete" rule concrete.
const vestibularProps = ['transform', 'scale', 'translate', 'rotate', 'background-position'];
function reducedVersion(anim) {
const risky = anim.props.some((p) => vestibularProps.includes(p));
if (!risky) return { ...anim, note: 'keep as is' };
if (anim.meaning) {
return { ...anim, props: ['opacity'], note: 'replace with fade' };
}
return { ...anim, props: [], note: 'remove (decorative)' };
}
const animations = [
{ name: 'modal-enter', props: ['transform', 'opacity'], meaning: true },
{ name: 'logo-pulse', props: ['scale'], meaning: false },
{ name: 'link-hover', props: ['color'], meaning: true },
];
for (const a of animations) {
const r = reducedVersion(a);
console.log(r.name.padEnd(12), '->', r.props.join(',') || '(none)', '|', r.note);
}
How to enable or disable reduced motion on your device#
The browser reads the preference from the device, so you turn it on in the OS (or in Firefox directly). These are the paths listed by MDN:
| Platform | Where |
|---|---|
| Windows 11 | Settings > Accessibility > Visual Effects > Animation Effects |
| Windows 10 | Settings > Ease of Access > Display > Show animations in Windows |
| macOS up to 15 (Sequoia) | System Settings > Accessibility > Display > Reduce motion |
| macOS 25 (Tahoe) onwards | System Settings > Accessibility > Motion > Reduce motion |
| iOS | Settings > Accessibility > Motion |
| Android 9+ | Settings > Accessibility > Remove animations |
| GTK/GNOME | Settings > Accessibility > Seeing > turn on "Reduced animation" |
| Plasma/KDE | System Settings > Workspace Behavior > General Behavior > set "Animation speed" to "Instant" |
On Windows, note the wording is inverted. The toggle is about showing animations, so you turn animation effects off to request reduced motion. To disable the preference again (back to no-preference), flip the same setting back.
For Linux there are a few extra options. In older GNOME versions, turn Animations off in GNOME Tweaks. You can also add gtk-enable-animations = false to the [Settings] block of the GTK 3 configuration file. To make Firefox and other GTK 4 programs respect the setting, try:
gsettings set org.gnome.desktop.interface enable-animations false
On KDE you can add AnimationDurationFactor=0 to the [KDE] block of ~/.config/kdeglobals, or run kwriteconfig6 --key AnimationDurationFactor 0.
Chrome has no separate switch in the documentation I'm working from. It follows the device setting, so change it in the OS table above.
How to test prefers-reduced-motion#
The quickest path is the Firefox override. Open about:config, add a number preference called ui.prefersReducedMotion, and set it to 1 to indicate a preference for reduced motion or 0 for full animation. Changes take effect immediately, so you can toggle it with your page open and watch the result. That also makes it a handy way to exercise the change listener from the JavaScript section.
Otherwise, flip the OS setting from the table and reload. A checklist I'd run through:
- Turn the setting on and load the page. Confirm large movement (scale, pan, slide) is gone or replaced.
- Check that state changes are still visible: menus open, errors show, selections highlight.
- With the page open, toggle the setting and confirm the JavaScript side reacts without a reload.
- Turn it off again and confirm full animation returns.
The MDN example is a good smoke test too. Its colors change when the media query matches, which is a trick worth copying: while debugging, temporarily give the reduce block an obvious background color so you can see at a glance that it's applying.
Common mistakes with prefers-reduced-motion#
Wrong source order. The reduce block needs the same specificity as the base rule and has to come later. Otherwise the base rule wins.
Removing everything. Setting animation: none and transition: none on every element is tempting. It also removes feedback that tells users something changed. Prefer swapping to opacity or color changes for anything meaningful.
Forgetting script-driven motion. CSS media queries don't stop a requestAnimationFrame loop or an auto-advancing carousel. Check matchMedia in those places.
Assuming reduce means "no motion at all". The setting is about non-essential motion. If a spinner shows that work is in progress, that's information. Make it calmer rather than deleting it.
Testing only with the setting off. Most developers never turn it on, so the reduce branch quietly rots. Test it as part of normal review.
Mixing up Windows wording. Turning Animation Effects off is what requests reduced motion there. If your test shows no change, double-check which way the toggle went.
Quick reference#
- Syntax:
@media (prefers-reduced-motion: reduce) { ... }. Values arereduceandno-preference. @media (prefers-reduced-motion)alone equalsreduce.- Put the reduce block after the base rule, with the same specificity.
- Replace scaling and panning with a fade. Remove looping decoration. Keep gentle feedback.
- In JS:
window.matchMedia('(prefers-reduced-motion: reduce)'), read.matches, listen forchange. - Test in Firefox with
ui.prefersReducedMotion(1reduce,0full animation), or use the OS setting. - Verify state changes are still visible with the preference on.
For broader background, MDN's page links to the CSS-Tricks introduction and the WebKit blog post on responsive design for motion, both worth a read after this.
Quick answers
What does prefers-reduced-motion: reduce mean?
It matches when the user has enabled a reduced-motion setting on their device. It signals that they'd like motion-based animation removed, reduced, or replaced, often because large movement can trigger vestibular discomfort.
How do I enable prefers-reduced-motion in Chrome?
Turn on the reduce-motion setting in your operating system, since the browser reads the device preference. On Windows 11 that's Settings > Accessibility > Visual Effects > Animation Effects, and on macOS up to 15 it's System Settings > Accessibility > Display > Reduce motion.
How do I test prefers-reduced-motion in Firefox?
Open about:config, add a number preference named ui.prefersReducedMotion, and set it to 1 for reduced motion or 0 for full animation. Changes take effect immediately.
What is the difference between reduce and no-preference?
reduce means the user enabled the setting and evaluates as true. no-preference means no preference was expressed and evaluates as false, so the query @media (prefers-reduced-motion) alone is the same as reduce.
Can I read prefers-reduced-motion in JavaScript?
Yes. Call window.matchMedia('(prefers-reduced-motion: reduce)') and check its matches property, and add a change listener if you want to react when the user toggles the setting.
Did this solve it?
One tap. No account.
Sorry about that. What was missing?
from 0 readers
Saved you an hour? Send a tip.
USDC or ETH on Base. Keeps the notes free and the examples running.

0x9dFD2Ee86182f92464b50809C6702B999a0Cd54f
On a computer? Scan the code with your phone wallet, or copy the address.
USDC or ETH on Base works best. Other tokens on Base, Ethereum or Arbitrum also reach this address. Tips are optional and unlock nothing; every guide stays free.
Discussion
Start the discussion
Next note