Using the prefers-reduced-motion Media Query to Honour User Motion Preferences
Write CSS that respects the prefers-reduced-motion media query, with WCAG 2.3.3 guidance on essential versus non-essential animation.
The prefers-reduced-motion CSS media query reads a user’s operating system setting. It carries real weight under WCAG 2.3.3, “Animation from Interactions.” That criterion says motion animation triggered by interaction can be disabled, unless the animation is essential to the functionality or the information being conveyed. The key word is “essential”, not “nice,” not “decorative,” not “the designer worked hard on it.” Essential means the animation conveys something that words, layout, or static state cannot. A loading spinner that communicates progress qualifies. A positional cue that tells a user where they are in a multi-step flow qualifies. If you can remove the animation and the page still makes sense, the animation is non-essential. Your CSS must honour the reduce preference. This is a baseline requirement. The page you are reading shows you, in runnable CSS, exactly how to write that media query, what “essential” looks like in practice, and what pitfalls make an otherwise-correct rule fail in the real world.
The Specification and the User’s Intent
The prefers-reduced-motion CSS media query is defined in Media Queries Level 5, a W3C Candidate Recommendation Snapshot dated 2024-12-17. The specification lists two values: no-preference (the initial state, signalling the user has not requested reduced motion) and reduce (the user has enabled a setting in their operating system to minimise motion). The MDN documentation, backed by Blink, WebKit, and Gecko compatibility data, confirms support across all major engines since roughly 2019. You do not need a feature detection guard, a JavaScript library, or a polyfill. The moment you write @media (prefers-reduced-motion: reduce), you are speaking a language every modern browser understands. The user’s intent arrives in your CSS as a simple boolean. It does not matter whether the setting was toggled in Windows, macOS, iOS, Android, or a Linux desktop environment. Ignoring it is a functional failure for the one in thirteen adults who experiences vestibular motion sensitivity. The W3C WAI guidance explains that vestibular disorders are triggered by motion in the periphery, not just the centre of the screen.
What You Must Disable: The Declaration That Removes Non-Essential Motion
Start With A Blanket Rule
The safest baseline, the one that demonstrates you have read WCAG 2.3.3 and taken it seriously, is a blanket rule that kills every animation and transition unless a more specific rule re-enables it. Here is the complete, runnable sample that does exactly what the majority of accessibility audits expect:
@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;
}
}
This replaces the unguarded animation that would have run. A slow fade-in on page load. A parallax background that shifts as you scroll. A button that pulses to draw attention. Each of those, left alone, runs at its full duration and forces the user’s vestibular system to process motion they never asked for. Setting animation-duration to a near-zero value and capping the iteration count at one removes the motion while preserving the element’s final state. The animation still applies its from keyframe, but the transition is so fast it is imperceptible. The !important flag is worth arguing about, but in practice it is the only way to beat author styles that are sloppy with specificity. Do not wrap this in @supports (prefers-reduced-motion). That adds no value and breaks in older browsers that support the media feature but not the @supports rule.
The Failure Case: Re-Adding Motion Without A Second Query
Here is the trap that separates a competent implementation from a broken one. You write the blanket reduce rule above. Then you add a loading spinner to your page, and you want it to spin because it communicates functional progress. You write animation: spin 1s linear infinite; inside the reduce block. Or you add a class that applies it. The user’s explicit request for reduced motion is broken. Preserve the spinner only if you can prove it is essential. If you can, gate it behind a separate @media (prefers-reduced-motion: no-preference) query. That query is the default for users who have not asked for reduction. The reduce query means “I have asked for less motion,” not “I have asked for no motion at all, ever.” A progress indicator that tells a user their file is uploading is essential. A decorative bounce on a testimonial card is not. If you are unsure which side a particular animation falls on, ask whether the user would lose information if the animation were static. If the answer is no, it goes away. This failure case gets reported as “the site ignores my accessibility settings.” Respect the cascade: the reduce block sets the baseline. Any re-enablement must be a conscious, justified override.
Replacing a Bounce Entrance with a Static Fade
Swap The Keyframe, Not The Element
A bounce entrance drops an element in from above and overshoots before settling. It is pure decoration. It signals “attention,” but a static fade signals the same thing without the risk of triggering motion sensitivity. Here is the sample that does the replacement, complete and runnable on its own:
/* Unprotected: the bounce that would have run */
@keyframes bounce-in {
0% { transform: translateY(-20px); opacity: 0; }
60% { transform: translateY(5px); opacity: 0.7; }
100% { transform: translateY(0); opacity: 1; }
}
.hero-title {
animation: bounce-in 0.8s ease-out;
}
/* Under prefers-reduced-motion: replace bounce with a fade */
@media (prefers-reduced-motion: reduce) {
.hero-title {
animation-name: fade-in;
animation-duration: 0.5s;
}
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
This replaces the unguarded bounce that would have run. That bounce used transform to move the element up and down, a property that triggers layout and paint on the main thread. The reduce version uses only opacity. The browser can handle the animation on the GPU without forcing a repaint. The user still sees content appear. There is no spatial displacement. No sudden shift in the document flow. The duration drops, but the primary fix is removing the translateY motion entirely. If you have a design-system component with an existing bounce, this is the pattern to ship. Keep the keyframes for the default case. Override the animation-name in the reduce query. Do not bother with a no-preference query here. The default behaviour is already the motion-heavy one.
Why Opacity and Transform Are Your Only Safe Bet for Motion
The compositor paints pixels to the screen. When you animate transform and opacity, the compositor handles the work without asking the main thread to re-layout the page. Animating width, height, top, left, or margin forces the browser to recalculate layout on every frame. That is expensive. It is also unnecessary for most motion. When you honour a reduced-motion preference, you are also writing faster CSS. The reduce block can replace a main-thread animation with a compositor-only one. The fade sample above is a perfect example. The bounce used transform, which is compositor-safe, but it moved the element in space. The fade uses opacity, also compositor-safe, but it avoids spatial change entirely. If an animation must move something, ask whether the movement is essential. If it is, scale it down to a fraction of the original distance. Ten percent instead of one hundred percent. The motion is less likely to trigger a response. This is a well-documented accommodation for the share of the population who experience some form of motion sensitivity, according to the W3C’s own reference material.
Preserving the Loading Indicator: Essential Motion That May Still Run
Rotate Becomes Pulse
Some animations communicate information a static state cannot. A loading spinner says “work is in progress, do not close the tab.” A progress bar says “you are 40% of the way through the upload.” Under WCAG 2.3.3, these are essential. Without them the user does not know whether the system is responding. Here is the sample that preserves the spinner without violating the user’s preference:
/* The unguarded animation: a spinner that runs for everyone */
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.loading-indicator .spinner {
animation: spin 1s linear infinite;
}
/* Under prefers-reduced-motion: keep it, but make it a pulsing dot instead */
@media (prefers-reduced-motion: reduce) {
.loading-indicator .spinner {
animation-name: pulse;
animation-duration: 2s;
animation-iteration-count: infinite;
}
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
This replaces the unguarded spinner that would have run. That spinner rotated the element continuously. The reduce version swaps rotation for a pulsing opacity change. The user still gets the information “something is loading.” The element changes state. The motion is confined to the foreground element’s opacity, not its position. A pulse is less likely to trigger vestibular motion sensitivity. The element stays put. Only its transparency changes. If you have a critical process that truly needs rotation, you must justify it as essential. You will find few cases where a static icon with a subtle pulse cannot do the job. Require every animation to earn its place by communicating something a static design cannot.
The Real Cost of Ignoring the Preference: Not Just Discomfort
When you skip the prefers-reduced-motion CSS media query, you are not making a user mildly annoyed. For someone with a vestibular disorder, motion in the periphery of their visual field can cause nausea, dizziness, and disorientation that lasts for hours after they leave your site. The W3C WAI guidance is explicit. This is a functional impairment, not a taste preference. WCAG 2.3.3 exists because the problem was common enough to warrant a normative success criterion. The cost of failing is not just a failed audit. You have excluded a group of users from accessing your content. On the practical side, a single page load with a full-screen parallax animation blocks scrolling and interaction. By honouring the reduce preference, you are making the page faster for the user who chose to reduce motion. You remove the very work that was janking their device. The performance and accessibility cases are the same argument.
The Fallback: Handling Browsers That Do Not Support the Media Query
A small percentage of users are on older device-locked browsers. Think iOS Safari on unsupported devices or Android WebView in apps that do not update. These browsers do not understand the prefers-reduced-motion media query. They ignore it. Your reduce rules never apply. The fallback is not to wrap the media query in @supports. That fails in older browsers that support the media feature but not the @supports rule. Instead, write the media query as-is. Accept that those users will get the default animation. If you must guarantee a reduction for those users, use JavaScript to read the operating system setting via window.matchMedia('(prefers-reduced-motion: reduce)') and add a class to the <html> element. That technique still works. It is now redundant for the vast majority of users. The accepted fallback is straightforward. Write your @media (prefers-reduced-motion: reduce) rule. Let browsers that support it do the right thing. For the others, the worst case is that they see a bounce entrance. That is the same experience they had before you wrote any code. Supporting the media query never makes things worse.
The Scroll-Driven Animation Problem
Newer CSS features, like scroll-driven animations, make the reduced-motion question more complex. These animations progress based on scroll position, using scroll() and view() timeline functions. They are off-main-thread, which sounds great for performance. But they are also motion. A user who has requested reduced motion should not have their scroll position hijacked by a decorative animation. The prefers-reduced-motion media query applies here just as it does to time-based animations. Wrap your scroll-driven animation in the media query. Set animation-timeline: none when the user prefers reduced motion. That disables it. The WCAG 2.3.3 criterion does not distinguish between time-based and scroll-based animation. If it is not essential, it must be disabled. Scroll-driven animations are so new that many developers have not yet applied the media query to them. The default experience is often exactly the kind of motion that triggers vestibular issues. If you are using scroll-driven animations, test them with the reduce preference enabled. Be prepared to swap them for a static state or a fade. The View Transitions API, which morphs between page states, is another example. The transition itself is a motion the user did not request. The reduce preference should disable it, even though the API requires JavaScript to trigger.
Honouring the Cascade: Specificity and Source Order
Place Reduce Rules Last
The CSS cascade determines which style wins when two rules conflict. A prefers-reduced-motion: reduce rule participates in the same cascade as every other rule on your page. A common mistake is to put the reduce rule early in your stylesheet. Later, a more specific selector re-enables an animation. Your own accessibility rule gets overridden. Put the reduce rules at the very end of your stylesheet, after all other animation styles. They get the last word. You can use a higher-specificity selector like html:root or use !important as a deliberate last resort. Understand that !important breaks the cascade and should be used sparingly. The other mistake is to rely on source order alone. A later developer adds a new component with an animation after your reduce block. The robust approach uses a dedicated @layer for reduced-motion rules. Layers have their own specificity quirks. A rule inside a layer does not have higher specificity than a rule outside. You still need to be careful. Keep all reduced-motion overrides in one file, at the end of the cascade. Test them with a real device set to reduce.
Testing and Debugging: What Actually Goes Wrong
Three Common Failures
When a prefers-reduced-motion rule does not work, the cause is rarely the media query itself. Three problems account for most failures. First, a custom property is set on a parent that does not inherit. A var() fallback silently fails. The animation uses the wrong duration. Second, a transition does not fire because the property value is not changing in a way that produces computed-value interpolation. For example, auto to 0 does not transition. display does not transition either. Third, the media query is written after a more specific rule. The cascade overrides it. To debug, open your browser’s developer tools. Enable the “emulate CSS media feature prefers-reduced-motion” option. Inspect the computed styles. If the animation-duration is still showing the full time, the rule is not being applied. Check specificity and source order. If it is applied but the animation still runs, the culprit is probably a transition on a property that is not transform or opacity. The failure modes are predictable. The fix is usually a one-line change. The media query itself is not a guarantee. It is a gate that your other CSS must pass through.
The Legal and Practical Reality
Compliance Is Not Optional
The WCAG 2.3.3 success criterion is referenced in legal frameworks around the world. In the United States, Section 508 of the Rehabilitation Act incorporates WCAG 2.0. The current revision, 2.2, is being adopted in various jurisdictions. In the European Union, EN 301 549 mandates WCAG 2.1 AA for public sector bodies. The European Accessibility Act extends this to more products. Canada’s AODA and Japan’s JIS X 8341 follow similar patterns. Supporting prefers-reduced-motion is not a “nice to have” you can defer until a client asks. It is a baseline requirement for any public-facing website. Failing it is a legal risk, not just a UX problem. The cost of compliance is low. A few lines of CSS you write once and maintain. The cost of non-compliance is the risk of a lawsuit, a negative audit, or a public accessibility report. The W3C publishes guidance on how to meet the criterion. The pattern is well-documented. If you are unsure whether your animation is essential, disable it. The default state of the web should be accessible. Motion should be an enhancement, not a requirement.
The Interop Quirks: What the Spec Does Not Tell You
Interoperability between browsers for prefers-reduced-motion is good, but not perfect. All major engines support it. Subtle differences exist in how they report the value when the user has not made a choice. Some browsers report no-preference as the initial state. Others might report reduce if the operating system has a “reduce motion” setting enabled, even if the user did not explicitly set it for the browser. This is rare. It means you should not assume that a user who visits your site with a default setting has no-preference. The spec says that no-preference is the initial value. A browser could choose to honour the OS setting by default. Writing @media (prefers-reduced-motion: no-preference) is risky. You are explicitly requesting the animation for users who have not asked for it. It might also apply to users who have set a global OS preference. The safer pattern is to write your animations as the default. Override them in the reduce block. You do not need to write a no-preference query at all. You avoid the ambiguity. The prefers-color-scheme media query has the same issue. The same advice applies: design for the default, then override for the preference.
What Counts as Essential: A Practical Checklist
When you must decide whether a given animation is essential, WCAG 2.3.3 gives you the definition. Applying it requires judgment. Here is a table that covers the common cases:
| Animation Example | Essential? | Why / Alternative |
|---|---|---|
| Loading spinner | Yes, if it is the only indicator of progress | Use a pulsing dot instead of rotation |
| Progress bar (determinate) | Yes | Use a static bar that grows via width, no motion |
| Button hover effect (scale) | No | Use a colour change instead |
| Page transition (fade/slide) | No | Use a straight fade, no spatial movement |
| Parallax background | No | Disable entirely, keep static background |
| Scroll-driven reveal (opacity) | No | Remove the opacity change, show content immediately |
| Skeleton screen (shimmer) | No | Use a static grey block, no animation |
| Animated autoplay carousel | No | Pause the carousel, require user input to advance |
| Confetti on a button click | No | Remove the confetti, or make it a static image |
| Input error shake | No | Use a red border and an aria-describedby message instead |
This table is not exhaustive. It gives you a starting point. The rule of thumb: if the user can get the same information from a static state, the animation is not essential. If the animation is the only way to know something is happening, it might be essential. Still look for a less motion-heavy alternative. The goal is not to remove all motion. Remove all non-essential motion. Make the essential motion as gentle as possible.
The Honest Caveat: When Reduce Is Not Enough
The prefers-reduced-motion CSS media query is a powerful tool. It is not a silver bullet. First, it only works if the user has set the preference in their operating system. If they have not, the browser reports no-preference. Your motion runs, regardless of how sensitive they are. Second, the media query does not remove all motion. It removes the motion your CSS controls. JavaScript-driven animations, canvas drawing, and WebGL effects are outside its reach. A user with a vestibular disorder will still see a full-screen canvas animation you wrote in JavaScript. The media query will not help. Third, the W3C is actively working on new features like prefers-reduced-data and prefers-reduced-transparency. These are not yet a substitute for the motion preference. The media query is a baseline, not a complete solution. You must still audit your CSS and JavaScript for motion. Test with real devices. Be willing to make manual changes. A user who has set reduce is not looking for a perfect experience. They are looking for one that does not make them sick. The media query is your first step, but not your last.