prefers-reduced-motion — Accessible Animations
Also Known As
TL;DR
Explanation
Motion on screen — parallax, auto-playing animations, rapid transitions — can trigger dizziness, nausea, and migraines in users with vestibular disorders (affecting up to 35% of adults over 40). The `prefers-reduced-motion: reduce` media query fires when the user enables 'Reduce Motion' in macOS/iOS, Windows 'Show animations', or Android accessibility settings. The correct approach is to retain meaningful transitions (opacity fades, simple slides) but remove spinning, bouncing, zooming, and parallax effects. JavaScript can read the preference via `window.matchMedia('(prefers-reduced-motion: reduce)')`. WCAG 2.1 Success Criterion 2.3.3 (Animation from Interactions) at AAA level requires an option to disable motion; at AA level, WCAG 2.3.1 restricts content that flashes more than 3 times per second.
Common Misconception
Why It Matters
Common Mistakes
- Setting `animation: none` globally under `prefers-reduced-motion` — removes useful feedback like loading spinners and state transitions.
- Only disabling CSS animations but not JavaScript-driven animations (GSAP, Framer Motion, custom `requestAnimationFrame` loops).
- Not pausing auto-playing carousels and video backgrounds — these are often more problematic than CSS animations.
- Testing only in a browser without actually enabling the OS-level reduced motion setting — the media query won't fire in browser devtools alone on some platforms.
Code Examples
/* No motion consideration — spins indefinitely */
.loader {
animation: spin 1s linear infinite;
}
.hero {
animation: parallax-float 4s ease-in-out infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes parallax-float { 50% { transform: translateY(-20px); } }
/* Default: full animation */
.loader {
animation: spin 1s linear infinite;
}
.hero {
animation: parallax-float 4s ease-in-out infinite;
}
/* Reduced motion: keep functional feedback, remove vestibular triggers */
@media (prefers-reduced-motion: reduce) {
.loader {
animation: fade-pulse 2s ease-in-out infinite; /* subtle opacity, no spin */
}
.hero {
animation: none; /* parallax removed entirely */
}
* {
transition-duration: 0.01ms !important; /* near-instant transitions */
scroll-behavior: auto !important;
}
}
@keyframes fade-pulse { 50% { opacity: 0.5; } }
// JavaScript: respond to preference
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)');
if (prefersReduced.matches) pauseCarousel();