Honoring prefers-reduced-motion for Accessibility
Up to 35% of adults experience vestibular disorders (inner ear balance issues), and motion-heavy animations can trigger dizziness, nausea, and disorientation. The CSS media query prefers-reduced-motion allows users to signal that they prefer minimal motion. Respecting this preference is not optional—it is a Web Content Accessibility Guidelines (WCAG) 2.1 requirement for Level AAA compliance. This article teaches you how to detect the preference and adapt your animations in React.
What Is Vestibular Disorder?
A vestibular disorder affects the inner ear's ability to maintain balance and spatial orientation. When a person with a vestibular disorder experiences certain animations (especially parallax, fast rotations, or rapid zooms), their brain receives conflicting signals: the eyes see motion, but the inner ear does not sense it. This mismatch causes vertigo, dizziness, or nausea—sometimes lasting hours. Respecting prefers-reduced-motion is not a courtesy; it is accessibility.
The prefers-reduced-motion Media Query
The prefers-reduced-motion media query detects whether the user has enabled "Reduce motion" in their operating system settings:
- Windows 11: Settings → Ease of Access → Display → Show animations
- macOS: System Preferences → Accessibility → Display → Reduce motion
- iOS: Settings → Accessibility → Motion
- Android: Developer options → Animation scale (unofficial)
CSS Implementation
/* Default: with animation */
.button {
transition: transform 0.3s ease;
}
.button:hover {
transform: scale(1.1);
}
/* Respecting prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
.button {
transition: none; /* Disable animation */
}
.button:hover {
transform: none; /* Jump to final state instantly */
}
}
JavaScript Detection
Detect the preference in JavaScript:
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!prefersReducedMotion) {
// Animation is safe; proceed
element.animate([...], { duration: 1000 });
} else {
// User prefers no animation; jump to final state
element.style.transform = 'translateX(100px)';
}
Implementing in React with CSS
The simplest approach: disable animations via CSS media query without touching React code:
import './button.css';
export default function AccessibleButton() {
return <button className="animated-button">Click me</button>;
}
.animated-button {
transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.animated-button:hover {
transform: scale(1.05);
}
@media (prefers-reduced-motion: reduce) {
.animated-button {
transition: none;
}
.animated-button:hover {
transform: none;
}
}
When the user has reduced motion enabled, the button jumps to the hovered state instantly. This is much better than rendering a janky 0.3 second animation for someone whose system cannot tolerate motion.
Implementing in React with Custom Hooks
For JavaScript-driven animations, create a hook to detect the preference and skip animations accordingly:
import { useEffect, useState } from 'react';
function usePrefersReducedMotion() {
const [prefersReduced, setPrefersReduced] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
setPrefersReduced(mediaQuery.matches);
const handleChange = (e) => {
setPrefersReduced(e.matches);
};
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}, []);
return prefersReduced;
}
export default usePrefersReducedMotion;
Now use this hook in components to conditionally animate:
import { useRef } from 'react';
import usePrefersReducedMotion from './usePrefersReducedMotion';
export default function ModalWithAccessibleAnimation() {
const modalRef = useRef(null);
const prefersReducedMotion = usePrefersReducedMotion();
const handleOpen = async () => {
if (prefersReducedMotion) {
// Jump to final state; no animation
modalRef.current.style.opacity = '1';
modalRef.current.style.transform = 'scale(1)';
} else {
// Animate normally
await modalRef.current.animate(
[
{ opacity: 0, transform: 'scale(0.9)' },
{ opacity: 1, transform: 'scale(1)' },
],
{
duration: 300,
easing: 'ease-out',
fill: 'forwards',
}
).finished;
}
};
return (
<div>
<button onClick={handleOpen}>Open Modal</button>
<div ref={modalRef} style={{ opacity: 0, display: 'none' }}>
Modal content
</div>
</div>
);
}
Providing Safe Alternatives
Do not just disable all motion. Instead, offer safe alternatives:
Alternative 1: Static Transitions
Replace motion with a quick state change (no animation):
export default function SafeButton() {
const [isHovered, setIsHovered] = useState(false);
const prefersReducedMotion = usePrefersReducedMotion();
if (prefersReducedMotion) {
// No motion: change color instantly
return (
<button
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{
backgroundColor: isHovered ? '#ff6b6b' : '#4ecdc4',
}}
>
Hover me
</button>
);
}
// Normal hover animation
return (
<button
style={{
backgroundColor: isHovered ? '#ff6b6b' : '#4ecdc4',
transition: 'background-color 0.3s ease',
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
Hover me
</button>
);
}
Alternative 2: Simpler, Slower Motion
Instead of disabling animation entirely, offer a slower, simpler version:
const ANIMATION_DURATION = prefersReducedMotion ? 100 : 300; // Much shorter duration
const ANIMATION_EASING = prefersReducedMotion ? 'linear' : 'cubic-bezier(0.25, 0.46, 0.45, 0.94)'; // No bounce
element.animate([...], {
duration: ANIMATION_DURATION,
easing: ANIMATION_EASING,
});
Slow, linear animations are safer for people with vestibular issues because they are predictable and do not create the illusion of acceleration.
WCAG Compliance Checklist
To meet WCAG 2.1 Level AAA for animation accessibility:
- All animations respect the
prefers-reduced-motionmedia query - Autoplay animations (e.g., carousels, looping videos) default to paused
- Animations with a duration > 5 seconds include pause/resume controls
- No animation flashes at > 3 flashes per second (photosensitivity risk)
- Parallax effects are subtle (offset < 200 pixels) or disabled for reduced motion
- Test with reduced motion enabled to confirm animations are disabled or safely degraded
Testing prefers-reduced-motion
Chrome DevTools Method
- Open Chrome DevTools (F12).
- Press Ctrl+Shift+P (Windows) or Cmd+Shift+P (macOS) to open the Command Palette.
- Type "emulate media feature prefers-reduced-motion" and press Enter.
- Select "prefers-reduced-motion: reduce".
Now reload your page. All animations should be disabled or safely degraded.
Manual OS Setting (More Realistic)
Test on your actual operating system:
Windows 11:
- Settings → Ease of Access → Display.
- Toggle "Show animations" off.
macOS:
- System Preferences → Accessibility → Display.
- Check "Reduce motion".
Reload your page and verify that animations are disabled.
Key Takeaways
- Up to 35% of adults have vestibular disorders; fast animations can trigger dizziness and nausea.
- The
prefers-reduced-motionmedia query detects user preference for reduced motion. - Implement
prefers-reduced-motion: reducein CSS to disable transitions and animations. - Use a custom React hook to detect the preference in JavaScript and skip WAAPI animations.
- Offer safe alternatives: instant state changes or slower, simpler animations.
- Test with reduced motion enabled via DevTools or OS settings before shipping.
Frequently Asked Questions
Does disabling all animations violate any design guidelines?
No. WCAG explicitly requires respecting prefers-reduced-motion. Providing a static, instant transition is an acceptable fallback. Many large sites (Apple, Google, GitHub) disable animations entirely for users with this preference.
What about users without vestibular disorders who just prefer no animation?
If a user has reduced motion enabled on their OS, they have indicated a preference for minimal motion, whether due to a disorder, personal preference, or device battery concerns. Always respect it.
Can I use animations on hover if I respect prefers-reduced-motion?
Yes, but only if the hover animation is subtle. Consider using transition: color (paint-triggering but quick) or transition: opacity (compositor-friendly) instead of transform (which can feel like movement). Or disable hover animations entirely when prefers-reduced-motion: reduce is active.
What about animations triggered by scroll or parallax effects?
Parallax effects (where elements move slower than the scroll) are especially problematic for vestibular disorders. Disable parallax entirely when prefers-reduced-motion: reduce is active:
@media (prefers-reduced-motion: reduce) {
.parallax-element {
transform: none; /* No parallax offset */
}
}
How do I handle video autoplay with prefers-reduced-motion?
Autoplay videos that move rapidly can trigger vestibular issues. Always mute videos on autoplay and provide a pause button. Consider not autoplaying video at all for users with prefers-reduced-motion: reduce:
<video
autoPlay={!prefersReducedMotion}
muted
controls
>
<source src="video.mp4" type="video/mp4" />
</video>
Is there a detection API that also tracks volume settings?
No. prefers-reduced-motion is the only standardized preference media query for motion. There is no API to detect autism (which can involve vestibular sensitivity), ADHD, or other conditions. Respecting prefers-reduced-motion is the baseline; offer a user preference toggle in your app settings for additional control.