For people with vestibular disorders, large on-screen movement can trigger real physical symptoms: dizziness, nausea, headaches, sometimes for hours. Parallax backgrounds that move at a different speed from the page, a card that zooms to fill the screen, a page that smooth-scrolls a long distance, or a background video that never stops can all do it. Others find constant motion distracting enough that they can’t read the page, which is a real problem for people with attention-related disabilities.
Every major operating system has a setting for this: “Reduce motion” on macOS and iOS, “Show animations in Windows” (turned off) on Windows, “Remove animations” on Android, and the equivalent in GNOME. Browsers expose the setting to CSS as a media feature, so you can respect it.
The media query
@media (prefers-reduced-motion: reduce) {
/* calmer styles */
}The feature has two values: no-preference (the default) and reduce. Note what it doesn’t say: it doesn’t say “no motion”. It says the user has asked for less, and it’s up to you to decide what less means for each animation. That decision is what interviewers want to hear you reason about.
You can write it either way round. The “opt in to motion” style puts animations inside the no-preference query, so if anything goes wrong (an old browser, a missing query), the default is the calm version:
.toast {
opacity: 0;
transition: opacity 200ms ease-out;
}
.toast.is-visible {
opacity: 1;
}
@media (prefers-reduced-motion: no-preference) {
.toast {
transform: translateY(16px);
transition: opacity 200ms ease-out, transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.toast.is-visible {
transform: translateY(0);
}
}With no preference set, the toast fades in while sliding up 16px. With reduce set, it only fades. Both users get a clear signal that a toast appeared; one of them doesn’t get the movement.
What to reduce, and what to keep
The useful distinction is between motion that moves things across the screen or changes their scale, and changes that happen in place.
Remove or replace:
- Parallax and scroll-linked effects, where elements move at different speeds as the user scrolls. These are the most commonly cited triggers.
- Large zoom and slide transitions, such as a thumbnail expanding into a full-screen view, or page transitions that slide the whole viewport.
- Smooth scrolling over long distances (
scroll-behavior: smoothonhtml). Jumping instantly is fine. - Auto-playing carousels, looping background videos, and animated illustrations that run continuously.
- Spinning, bouncing or shaking, especially when it repeats.
Usually fine to keep:
- Colour and opacity fades.
- Small, quick transitions on interactive elements, such as a button’s background changing on hover.
- Progress indicators. Replace a spinning loader with a pulsing opacity or a static “Loading…” label, but don’t remove the indicator: the user still needs to know something is happening.
The goal is that information conveyed by motion survives without it. If a panel slides in from the right to show it came from the “Details” button, a reduced version might cross-fade it and move focus to it. If a form shakes on an error, the reduced version should still show the error message and the red border; the shake was decoration on top of that.
Why the blanket reset is a trap
A snippet that circulates widely:
@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;
}
}It’s better than nothing as a safety net, and many sites ship it. But it has costs worth knowing about:
- It removes useful feedback along with harmful motion. Opacity fades that help users notice a toast or a menu now happen instantly.
- Animations that end in a state (“fade in from opacity 0 with
animation-fill-mode: forwards”) still run, just in 0.01ms, so they reach their end state. Animations that rely onanimationendevents still fire. That’s why 0.01ms is used instead of 0: some code waits for those events. !importanton every element makes legitimate reduced-motion styles harder to write later.
A considered approach is to treat each animation deliberately, perhaps keeping the reset as a fallback for third-party widgets.
JavaScript-driven motion
CSS media queries don’t reach motion started from script. Read the same preference with matchMedia:
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
function scrollToSection(el) {
el.scrollIntoView({ behavior: reduceMotion.matches ? 'auto' : 'smooth', block: 'start' });
}
reduceMotion.addEventListener('change', (event) => {
// the user changed the OS setting while the page is open
if (event.matches) heroVideo.pause();
});The change listener matters on long-lived pages: users do change the setting mid-session, especially after a page has made them feel unwell. Animation libraries have built-in support (Framer Motion’s useReducedMotion, GSAP’s matchMedia), and the Web Animations API can be checked the same way before calling element.animate().
Auto-playing video deserves a specific mention. Respect the preference by not autoplaying when it’s set, and in every case provide a visible pause control: WCAG 2.2.2 (Pause, Stop, Hide) requires a way to stop any moving content that starts automatically and lasts more than five seconds, whatever the user’s OS setting.
Testing it
You don’t need to change your OS setting each time. In Chrome DevTools, open the Command Menu (Cmd+Shift+P or Ctrl+Shift+P), run “Show Rendering”, and set “Emulate CSS media feature prefers-reduced-motion” to reduce. Playwright and Puppeteer can emulate it in automated tests (page.emulateMedia({ reducedMotion: 'reduce' }) in Playwright), which is a cheap way to snapshot both versions.
Then go through the page with reduce on and ask, for each animated element: does anything still move across the screen? Is any information now missing, such as where a panel came from, or that a background job is running?
Interview practice
A landing page has a hero with a parallax background, feature cards that slide up into view as you scroll, a “Back to top” link with smooth scrolling, and a lottie animation that loops forever beside the sign-up form. The team wants to support reduced motion “with one media query”. What do you advise?
Answer
One media query can catch the CSS cases, but each element needs a decision, and two are in JavaScript. The parallax background should become static: the image stays, the scroll-linked movement goes. The cards’ slide-up should become a simple fade, or they should just be visible from the start, since their arrival carries no information. The “Back to top” smooth scroll should jump instantly when the preference is set, which needs scroll-behavior: auto in CSS and a matchMedia check if it’s done with scrollIntoView or window.scrollTo in script. The looping lottie animation should not autoplay under reduce; show a static frame, and give everyone a pause button, since a loop longer than five seconds needs one under WCAG 2.2.2 regardless of the setting. A blanket reset could be added as a backstop for anything missed, but if it’s the only measure, the cards’ fade (harmless and useful) disappears along with the parallax (harmful). Finally, verify by emulating the preference in DevTools and in an automated test.
Keep going
- HTML and CSS topic hub
- HTML and CSS multiple-choice questions
- CSS transforms and animations
- CSS focus visibility and keyboard navigation
- MDN: prefers-reduced-motion