prefers-reduced-motion is a CSS media feature that reports whether a visitor has asked their device for less motion. It has two values, no-preference and reduce. Wrap large movement such as parallax, zooms and slides in a query, and when the value is reduce, swap them for a short fade or no animation.
| System | Setting |
|---|---|
| Windows 11 | Settings > Accessibility > Visual effects > Animation effects (off) |
| macOS | System Settings > Accessibility > Display (or Motion) > Reduce motion |
| iOS | Settings > Accessibility > Motion > Reduce Motion |
| Android 9 and later | Settings > Accessibility > Remove animations |
| Firefox (testing) | about:config, set ui.prefersReducedMotion to 1 |
Updated
People turn on reduced motion because sliding, zooming and parallax can trigger dizziness, nausea or headaches, especially for those with vestibular disorders. The badge below reads your own device setting live, and the demo shows the same animation in full and reduced form, so you can compare before choosing a pattern.
The badge reads your device setting with matchMedia, and the preview switch shows the reduced animation without changing your system.
The card slides in only when no preference is set. Everyone else sees a gentle fade.
<style>
@keyframes fade { from { opacity: 0 } to { opacity: 1 } }
@keyframes slide-up { from { opacity: 0; transform: translateY(40px) } to { opacity: 1; transform: none } }
.card { padding: 20px; border-radius: 12px; background: #eff6ff; animation: fade 400ms ease both }
@media (prefers-reduced-motion: no-preference) {
.card { animation: slide-up 600ms cubic-bezier(.2,.8,.2,1) both }
}
</style>
<div class="card">This card arrives with motion only if your device allows it.</div>Useful when you cannot rewrite every animation. Durations become almost zero rather than none, so animationend and transitionend events still fire.
@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;
}
}matchMedia returns a live list. Its change event fires when the visitor flips the setting.
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
function scrollToTop() {
window.scrollTo({ top: 0, behavior: query.matches ? "auto" : "smooth" });
}
query.addEventListener("change", (event) => {
document.documentElement.dataset.motion = event.matches ? "reduce" : "full";
});Starts at true on the server so nothing animates before the browser has answered.
import { useEffect, useState } from "react";
const QUERY = "(prefers-reduced-motion: reduce)";
export function usePrefersReducedMotion() {
const [reduce, setReduce] = useState(true);
useEffect(() => {
const mql = window.matchMedia(QUERY);
setReduce(mql.matches);
const onChange = (e) => setReduce(e.matches);
mql.addEventListener("change", onChange);
return () => mql.removeEventListener("change", onChange);
}, []);
return reduce;
}| Full motion | Reduced version |
|---|---|
| Parallax background | Static background |
| Slide or zoom page change | Cross-fade, or an instant change |
| Element flies in on scroll | Short fade in place |
| Spinning loader | Pulsing opacity or a progress bar |
| Smooth scroll to anchor | Instant jump |
| Autoplaying background video | Poster image with a play button |
The trouble is movement that fills a large part of the view or moves in a direction the visitor did not cause: parallax backgrounds, full-page zooms, sliding page changes, spinning elements and content that jumps as you scroll.
Small, quick, user-started feedback, such as a button colour change or a checkbox tick, is rarely an issue and can usually stay.
Many sites add a reduce query at the end that sets every duration to almost zero. It works, but it is easy to miss one animation, and some scripts listen for animationend events that then fire instantly.
Writing the motion inside a no-preference query flips the default. Anyone whose browser does not report a preference still gets motion, and anything you forget falls back to the calm version.
CSS queries do not reach animations started from JavaScript, canvas or smooth scrolling calls. Check the media query before calling element.animate() or scrollIntoView with behavior: smooth.
Read the setting once at start and subscribe to its change event, so a person who switches it mid-visit is respected without a reload.
Two: no-preference, when the visitor has not set anything, and reduce, when they have asked for less motion. Writing @media (prefers-reduced-motion) on its own is the same as asking for reduce.
Call window.matchMedia("(prefers-reduced-motion: reduce)") and read its matches property. Add a change event listener to the returned object to react when the setting changes while the page is open.
No. The request is for less motion, not a frozen page. Keep colour changes, short fades and progress indicators, and remove or shrink movement such as sliding, scaling, spinning and parallax.
WCAG 2.3.3 Animation from Interactions, level AAA, asks that motion triggered by interaction can be turned off unless it is essential. Respecting the system setting is one of the listed ways to meet it. Separately, 2.2.2 Pause, Stop, Hide at level A covers motion that starts on its own.
Use the motion-safe: variant to apply an animation only when no preference is set, or motion-reduce: to override one when reduce is set, for example motion-safe:animate-bounce.
Related: CSS transitions playground, Scroll animations, Framer Motion examples, CSS media queries, Website accessibility checklist, CSS animation generator.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.