Using prefers-reduced-motion for Accessible Animation
Some users experience dizziness, nausea, or disorientation from animated interfaces. The prefers-reduced-motion CSS media feature lets you detect their system preference and respond with safer motion — without stripping all visual polish from your UI. This article covers practical CSS and JavaScript implementation across common component patterns.
Key Takeaways
prefers-reduced-motionis a CSS media query that reads an OS-level accessibility setting, with two values:reduceandno-preference.- The goal is to reduce or replace non-essential motion, not strip animation entirely — opacity fades and shortened durations remain safer alternatives.
- Use the progressive (opt-in) CSS approach to protect users who haven’t explicitly set a preference but may still be sensitive to motion.
- For JavaScript-driven animations,
window.matchMediaand listeners like Motion’suseReducedMotion()hook keep behavior in sync with system changes. - Chrome DevTools’ Rendering panel emulates the preference, so you can test without altering your OS settings.
prefers-reduced-motioncan help address WCAG 2.3.3 but not autoplaying or looping content, which still needs explicit pause controls under WCAG 2.2.2.
What prefers-reduced-motion Actually Does
prefers-reduced-motion is a mature, widely supported CSS media query — not a new API. It reads a system-level accessibility setting the user has enabled on their OS (macOS, Windows, iOS, Android, Linux). The two values are:
reduce— the user has requested less motionno-preference— no preference has been set
Worth noting: @media (prefers-reduced-motion) is shorthand equivalent to @media (prefers-reduced-motion: reduce).
The MDN Web Docs entry covers the full syntax and browser compatibility table, while Can I use confirms broad browser support across modern engines.
The Right Mental Model: Reduce, Don’t Remove
A common mistake is treating this as a kill switch for all animation. That’s not the goal. The guidance — including WCAG 2.3.3 (“Animation from Interactions,” Level AAA) — is to reduce or replace non-essential motion, particularly:
- Large-scale movement (parallax, zooming, sliding panels)
- Spinning or rotating elements
- Scroll-triggered animations that move content across the viewport
Opacity fades, color transitions, and shortened durations are generally safer alternatives. A modal that fades in instead of flying up from the bottom still communicates state change without triggering vestibular discomfort.
CSS Implementation: Two Approaches
Defensive (opt-out): Define animated styles by default, then override inside the media query.
.modal {
transform: translateY(20px);
opacity: 0;
transition: transform 300ms ease, opacity 300ms ease;
}
@media (prefers-reduced-motion: reduce) {
.modal {
transform: none;
transition: opacity 200ms ease;
}
}
Progressive (opt-in): Define static styles by default, only add motion when the user hasn’t opted out.
/* Static by default */
.modal {
opacity: 0;
transition: opacity 200ms ease;
}
@media (prefers-reduced-motion: no-preference) {
.modal {
transform: translateY(20px);
transition: transform 300ms ease, opacity 300ms ease;
}
}
The progressive approach is safer for users who haven’t set a preference but may still be sensitive to motion.
CSS custom properties make this scalable across a large codebase:
:root {
--duration: 300ms;
--easing: ease;
}
@media (prefers-reduced-motion: reduce) {
:root {
--duration: 0.01ms;
}
}
.drawer {
transition: transform var(--duration) var(--easing);
}
Detecting the Preference in JavaScript
For JS-driven animations, use window.matchMedia:
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)');
if (prefersReduced.matches) {
// skip or simplify animation
}
// React to live preference changes (e.g., user toggles OS setting)
prefersReduced.addEventListener('change', (e) => {
if (e.matches) {
// pause or replace animations
}
});
This is useful for carousels, scroll-triggered effects, or any animation driven outside CSS.
Discover how at OpenReplay.com.
Animation Libraries: Framer Motion and Motion.dev
If you use Motion for React (the library many still call Framer Motion — its documentation now lives at Motion.dev), the useReducedMotion() hook handles this cleanly:
import { motion } from "motion/react";
import { useReducedMotion } from "motion/react";
function Drawer({ isOpen }) {
const reduce = useReducedMotion();
return (
<motion.div
animate={{ x: isOpen ? 0 : -300, opacity: isOpen ? 1 : 0 }}
transition={reduce ? { duration: 0 } : { duration: 0.3 }}
/>
);
}
The hook reads the system preference reactively, so it stays in sync if the user changes their OS setting mid-session.
Testing with Chrome DevTools
You don’t need to change your OS settings to test. In Chrome DevTools:
- Open DevTools → Rendering tab (via the three-dot menu → More tools → Rendering)
- Find “Emulate CSS media feature prefers-reduced-motion”
- Set it to
reduce
Your page will respond immediately, letting you verify every animated component without touching system preferences.
A Note on WCAG Coverage
prefers-reduced-motion can help address WCAG 2.3.3, but it doesn’t cover everything. Autoplaying video, looping GIFs, and continuously moving content may still require explicit pause/stop controls under WCAG 2.2.2 (“Pause, Stop, Hide”). The media query handles interaction-triggered animation well — persistent background motion needs a separate solution.
Practical Takeaway
Audit your animated components — modals, drawers, hover effects, carousels, page transitions — and decide for each whether to reduce duration, swap movement for opacity, or skip the animation entirely. Use CSS custom properties to centralize the logic, matchMedia when JavaScript controls the animation, and Chrome DevTools to verify without changing your OS.
Conclusion
Respecting prefers-reduced-motion is one of the lowest-effort, highest-impact accessibility wins available to front-end developers. The technology is stable, browser support is excellent, and the implementation patterns are straightforward across CSS, JavaScript, and modern animation libraries. The real work is shifting your mental model: animation becomes a layer to dial down for sensitive users rather than a feature to toggle off. Build that habit into every component you ship.
FAQs
Setting duration to a very small value like 0.01ms is often preferred over zero. It preserves the transitionend event, so JavaScript logic that listens for animation completion continues to fire. A true zero can skip the event in some browsers and break state machines that depend on it. The visual result is identical to instant change.
It can, but behavior differs between browsers and implementations. A safer approach is to wrap CSS smooth scrolling inside @media (prefers-reduced-motion: no-preference) so reduced-motion users always receive instant scrolling. If you implement custom scroll animation in JavaScript, you should also check the preference manually and fall back to an instant scroll using window.scrollTo with behavior set to auto.
No. Shorter or skipped animations typically improve perceived responsiveness because users see content settle faster. Interaction to Next Paint often benefits since transitions no longer delay visual feedback. The only caveat is removing animations that communicated state changes — replace them with opacity fades or color shifts so the interface still feels responsive rather than abrupt.
Yes, and it's a good practice for users on shared devices or those who haven't discovered the OS option. Store the preference in localStorage and combine it with the media query result using logical OR. This way, either the OS setting or the in-app toggle can trigger reduced motion, giving users maximum control without overriding their system preference.
Truly understand users experience
See every user interaction, feel every frustration and track all hesitations with OpenReplay — the open-source digital experience platform. It can be self-hosted in minutes, giving you complete control over your customer data. . Check our GitHub repo and join the thousands of developers in our community..