How useReducedMotion Makes React Animations More Accessible
A modern guide to useReducedMotion, prefers-reduced-motion, and building React interfaces that respect accessibility without sacrificing great UX.
When Animation Becomes a Bug
For most developers, animation is a sign of polish.
Parallax hero sections. Smooth page transitions. Auto-playing carousels. Floating backgrounds. Loading spinners that bounce and rotate.
These effects make applications feel modern.
But for millions of users, they can have the opposite effect.
People with vestibular disorders, migraine sensitivity, Ménière’s disease, or motion sensitivity can experience nausea, dizziness, headaches, or even lose the ability to use a website altogether because of excessive animation.
That is exactly why every major operating system now includes a Reduce Motion accessibility setting.
Fortunately, modern browsers expose that preference through the CSS media query:
prefers-reduced-motionReact applications should respect it too.
In this article we’ll explore how to implement motion-aware interfaces using useReducedMotion, why simply checking matchMedia() isn’t enough, and how to scale the solution across an entire application.
Why Reduced Motion Exists
Operating systems have supported reduced-motion preferences for years.
Users can enable it in:
iOS
macOS
Windows
Android
GNOME and many Linux desktop environments
When enabled, the operating system is telling applications:
“Please minimize unnecessary movement.”
This doesn’t mean removing every transition.
Instead, it targets effects that commonly trigger motion sickness, including:
parallax scrolling
large zoom animations
autoplaying carousels
endlessly looping decorative animations
moving backgrounds
auto-scrolling interfaces
The browser exposes this preference through one media query:
@media (prefers-reduced-motion: reduce) {
...
}For CSS-only animations, that’s often all you need.
React becomes interesting when JavaScript controls the animation.
The Naive React Solution
Most developers eventually write something like this:
const [reduceMotion, setReduceMotion] = useState(false);
useEffect(() => {
const media = window.matchMedia(
'(prefers-reduced-motion: reduce)'
);
setReduceMotion(media.matches);
const handleChange = () =>
setReduceMotion(media.matches);
media.addEventListener('change', handleChange);
return () =>
media.removeEventListener(
'change',
handleChange
);
}, []);It works.
Mostly.
Until it doesn’t.
Three Problems With This Pattern
1. It isn’t SSR-friendly
During server rendering there is no window.
Every React framework eventually hits this issue:
Next.js
Remix
Astro
TanStack Start
Developers often scatter checks like this everywhere:
typeof window !== "undefined"That’s unnecessary complexity.
2. Many implementations never update
A common mistake is reading the value only once:
media.matchesand never subscribing to changes.
Most users never notice.
But if someone changes the operating system preference while your app is open, your UI stays out of sync.
3. Every component rewrites identical code
Animation-heavy applications often contain dozens of components.
Each one copies:
matchMedialisteners
cleanup logic
state management
The code becomes repetitive and difficult to maintain.
Enter useReducedMotion
A dedicated hook solves all of these problems.
The API couldn’t be simpler:
const reduceMotion =
useReducedMotion(defaultState?);It returns a boolean that automatically updates whenever the system preference changes.
During SSR it safely returns the default value until the client hydrates.
Internally the implementation is surprisingly small:
export function useReducedMotion(
defaultState?: boolean
) {
return useMediaQuery(
'(prefers-reduced-motion: reduce)',
defaultState
);
}The complexity lives inside useMediaQuery.
Every component simply consumes the result.
Pattern 1: Disable Individual Animations
The simplest use case is conditionally enabling visual effects.
function Hero() {
const reduceMotion = useReducedMotion();
return (
<section
className={
reduceMotion
? "hero"
: "hero hero--parallax"
}
/>
);
}Instead of merely hiding the effect, you avoid creating the animation entirely.
That means:
no scroll listeners
no
requestAnimationFramefewer layout calculations
better performance
Accessibility and performance improve together.
Pattern 2: Integrate With Animation Libraries
Libraries like Framer Motion or GSAP work perfectly with reduced-motion preferences.
<motion.div
initial={{
opacity: 0,
y: reduceMotion ? 0 : 24
}}
animate={{
opacity: 1,
y: 0
}}
transition={{
duration: reduceMotion
? 0
: 0.4
}}
/>Notice what changes here.
The fade animation remains.
Only the vertical movement disappears.
That’s exactly what accessibility guidelines recommend.
Users still receive visual feedback without unnecessary motion.
Pattern 3: One Global Motion Switch
Large applications may contain hundreds of animated components.
Instead of checking every animation individually, expose a single attribute at the root.
<div
data-motion={
reduceMotion
? "reduce"
: "normal"
}
>
{children}
</div>Now CSS can react globally.
[data-motion="reduce"] *,
[data-motion="reduce"] *::before,
[data-motion="reduce"] *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}Now both JavaScript and CSS share exactly the same source of truth.
No duplicated media queries.
No synchronization problems.
No inconsistent behavior.
Reduced Motion Doesn’t Mean Zero Motion
This is one of the biggest misconceptions.
The accessibility preference is not asking developers to remove every animation.
Good candidates for removal include:
parallax scrolling
autoplaying hero animations
infinite decorative loops
floating backgrounds
animated camera movement
auto-rotating banners
Animations that generally remain appropriate include:
hover transitions
button feedback
checkbox animations
loading indicators
drag-and-drop feedback
short opacity fades
A useful rule of thumb is simple:
If the animation covers a large part of the screen or keeps moving without user interaction, it’s probably a good candidate for reduction.
CSS Might Be Enough
Many developers immediately reach for JavaScript.
Sometimes they shouldn’t.
If your animations exist only in CSS, this is enough:
@media (prefers-reduced-motion: reduce) {
...
}or, with modern utility frameworks:
motion-reduce:JavaScript becomes useful only when animation affects rendering logic itself.
Examples include:
skipping expensive animation libraries
replacing autoplaying videos with static images
disabling canvas animations
avoiding scroll-driven effects entirely
SSR Safety
One advantage of a proper hook implementation is server compatibility.
Since matchMedia() is accessed inside an effect, it never executes during server rendering.
That means:
no hydration mismatch
notypeof windowchecksidentical server and client output
preferences applied immediately after hydration
This pattern scales naturally across every React framework.
Beyond Motion Preferences
useReducedMotion is just one member of a larger family of hooks that help React applications respect user preferences defined at the operating system level.
Some of the most useful ones include:
useReducedMotion— reads theprefers-reduced-motionmedia query and returns aboolean.usePreferredColorScheme— detectsprefers-color-schemeand reports whether the user prefers a light or dark interface.usePreferredContrast— exposes the user’s preferred contrast level, allowing your UI to adapt for improved readability.useMediaQuery— the underlying primitive that lets you evaluate any CSS media query and react to it from JavaScript.
These preferences should be treated as first-class application state, just like viewport size, network connectivity, or device capabilities. They aren’t optional enhancements—they’re explicit choices users have already made in their operating system.
The best user experiences don’t ask people to configure your application from scratch. Instead, they respect the accessibility and display preferences users have already chosen, making your app feel native from the very first render.
Final Thoughts
Accessibility isn’t just about screen readers or keyboard navigation.
Sometimes it’s as simple as not making someone physically uncomfortable.
The prefers-reduced-motion media query gives developers a straightforward way to respect a user’s operating system preference, and React hooks make that integration effortless.
Instead of sprinkling matchMedia() throughout your components, centralize the logic in a reusable hook, make it SSR-safe, and let the rest of your UI respond naturally.
The result is cleaner code, better performance, and an interface that’s welcoming to everyone—not just those who enjoy animated experiences.


