prefers-reduced-motion Recipes: Reduce, Don't Just Remove
You have shipped a parallax header, an autoplaying carousel, and a dozen hover transitions, and now you need every one of them to behave for users who have asked their operating system for less motion. The narrow problem this guide solves is supplying ready-to-paste blocks for the effects that actually cause trouble — parallax, autoplay, and transitions — built around one idea: reduce, do not blindly remove. This page sits under Accessibility in CSS Animations, and it puts the syntax from Reducing Motion Preferences in CSS to work on concrete components.
Recipes covered:
- A safe global reset that does not break
fill-mode - Disabling parallax while keeping content visible
- Pausing autoplay and swapping to a poster
- Shortening transitions instead of deleting feedback
The reduce-not-remove principle
The crude approach — wrap the whole site in a @media (prefers-reduced-motion: reduce) block that sets every duration to zero — is a defensible safety net, but it is rarely the best experience. A user who asked for less motion did not ask for an interface that feels broken. They asked you to stop moving things around the screen. A button can still acknowledge a click with an instant colour change; a panel can still cross-fade; a notification can still appear. What should stop is the travel, the parallax, the spin, the autoplay. Reducing rather than removing keeps the interface legible and responsive while honouring the request.
In practice this means treating the media query as a router: the same component takes the full-motion path or the reduced path, and the reduced path is a deliberately designed alternative, not an absence. The diagram shows the media query as that gate, sending one component down two designed routes.
Complete working implementation
A single page that wires up all three recipes plus the global net. Each block is independent — paste the ones you need.
<header class="parallax">Hero</header>
<figure class="autoplay">
<video class="autoplay__video" autoplay muted loop poster="/poster.jpg">
<source src="/loop.webm" type="video/webm">
</video>
<img class="autoplay__poster" src="/poster.jpg" alt="Product on a desk">
</figure>
<button class="cta">Buy now</button>
/* ---- Recipe 0: safe global net -------------------------------------
Near-instant rather than truly 0s, so animation-fill-mode: forwards
still resolves to the final frame without a flicker. */
@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;
}
}
/* ---- Recipe 1: parallax -> static --------------------------------- */
.parallax {
background-attachment: fixed; /* the parallax effect */
background-image: url("/hero.jpg");
}
@media (prefers-reduced-motion: reduce) {
.parallax {
background-attachment: scroll; /* image scrolls with content, no parallax */
}
}
/* ---- Recipe 2: autoplay video -> poster --------------------------- */
.autoplay__poster { display: none; }
@media (prefers-reduced-motion: reduce) {
.autoplay__video { display: none; } /* hide the moving video */
.autoplay__poster { display: block; } /* show the still frame instead */
}
/* ---- Recipe 3: transition -> shortened, not deleted --------------- */
.cta {
transition: background-color 0.25s ease, transform 0.25s ease;
}
.cta:hover { background-color: #1d49d8; transform: translateY(-2px); }
@media (prefers-reduced-motion: reduce) {
.cta {
transition: background-color 0.1s ease; /* keep colour feedback, drop move */
}
.cta:hover { transform: none; } /* remove the lift only */
}
Notice that none of the reduced paths is "nothing happens." Parallax becomes a normal scrolling image, the video becomes its own poster frame, and the button keeps its colour feedback while losing only the lift. That is reduce-not-remove in three concrete forms.
The technique that makes it work
The autoplay recipe shows the load-bearing move: rather than trying to stop the video with CSS (which cannot pause playback), you hide the moving element and reveal a still image that was already in the markup. CSS cannot control media playback, but it can control which of two siblings is displayed, so the swap is purely a display toggle inside the media query. For true autoplay control you would add a one-line JavaScript check — if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) video.pause() — but the CSS swap alone removes the motion from the user's view. Across all three recipes the pattern is identical: define the full experience as the default, then override only the motion-bearing properties inside the query.
Variation: a custom-property speed dial
For a design system, expose a single duration token and let the query dim it, so every component that reads the token reduces at once without per-component overrides.
:root { --motion: 1; } /* 1 = full speed */
@media (prefers-reduced-motion: reduce) {
:root { --motion: 0.01; } /* near-instant everywhere */
}
.fx { transition-duration: calc(250ms * var(--motion)); }
This pairs naturally with token-driven timing from Fluid Spacing Tokens Driving Transition Durations.
Browser support
prefers-reduced-motion is supported in Chrome 74+, Edge 79+, Firefox 63+, and Safari 10.1+. background-attachment: fixed and display toggles have universal support, and the matchMedia API for the optional JavaScript pause is supported everywhere evergreen. Unsupporting browsers ignore the @media blocks and render the full-motion defaults, so no @supports guard is needed.
FAQ
Should prefers-reduced-motion remove all animation?
Not always. The reduce-not-remove principle says to cut motion that implies movement while keeping useful feedback like fades or instant state changes. Full removal is fine as a safe default, but a thoughtful reduction is usually better UX.
How do I handle autoplaying media under reduced motion? Pause autoplay and offer a manual control. For looping background video or animated GIFs, swap to a static poster image inside the reduce block, and never auto-restart playback.
What is a safe global reset for reduced motion?
A universal selector block that sets animation-duration and transition-duration to 0.01ms with animation-iteration-count: 1, marked important. It neutralizes runaway motion site-wide while leaving a near-instant state change so fill-mode: forwards still applies.
Can I detect reduced motion in JavaScript too?
Yes. Use window.matchMedia('(prefers-reduced-motion: reduce)').matches to branch logic, and listen for changes so you respond if the user toggles the preference while the page is open.
Related
- Accessibility in CSS Animations — the parent guide on accessible motion.
- Reducing Motion Preferences in CSS — the underlying media-query syntax and cascade.
- Vestibular-Safe Animation Patterns — which motion to avoid before you ever need a reduction.
- Fluid Spacing Tokens Driving Transition Durations — token-driven timing that dims cleanly under reduced motion.
- Fluid Typography Without JavaScript — responsive scaling that needs no animated zoom.
Related articles
More pages in the same section.