Skip to main content

Reduced Motion Accessibility

Respecting user preferences for reduced motion using prefers-reduced-motion.

Why It Matters

Motion sensitivity can cause physical illness — nausea, headaches, disorientation — in people with vestibular disorders. This affects approximately 35% of adults over 40 and an estimated 69 million Americans.

This is a genuine medical need, not a preference. WCAG 2.1 Success Criterion 2.3.3 (AAA level) requires providing an option to turn off non-essential motion.

The prefers-reduced-motion Media Query

Users enable this via OS settings:

  • macOS: System Preferences → Accessibility → Display → Reduce motion
  • Windows: Settings → Ease of Access → Display → Show animations (off)
  • iOS: Settings → Accessibility → Motion → Reduce Motion
  • Android: Settings → Accessibility → Remove animations
/* Default: animations enabled */
.element {
transition: transform 0.3s ease-out;
animation: spin 2s linear infinite;
}

/* Reduced: disable or minimize */
@media (prefers-reduced-motion: reduce) {
.element {
transition: none;
animation: none;
}
}

Essential vs. Decorative Motion

Decorative animations (parallax scrolling, auto-playing backgrounds, hover sparkles) → disable entirely.

Essential animations (loading indicators, progress bars) → use subtle alternatives.

/* Decorative: disable completely */
@media (prefers-reduced-motion: reduce) {
.parallax-hero { background-attachment: scroll; } /* Remove parallax */
.animated-bg { animation: none; }
.hover-sparkle { display: none; }
}

/* Essential: replace with subtle alternative */
@media (prefers-reduced-motion: reduce) {
.loading-spinner {
animation: none;
/* Replace spinning with pulsing opacity */
animation: pulse 1s ease-in-out infinite;
}
}

@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}

Implementation Patterns

CSS Variables Approach

:root {
--duration: 300ms;
--easing: ease-out;
}

@media (prefers-reduced-motion: reduce) {
:root {
--duration: 0ms;
--easing: step-end;
}
}

.animated {
transition: transform var(--duration) var(--easing);
}

JavaScript Check

const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

function animateElement(element) {
if (prefersReducedMotion) {
// Instant state change, no animation
element.style.opacity = 1;
return;
}
// Full animation
element.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 300 });
}

React Hook

function usePrefersReducedMotion() {
const [reducedMotion, setReducedMotion] = useState(
window.matchMedia('(prefers-reduced-motion: reduce)').matches
);

useEffect(() => {
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
const handler = (e) => setReducedMotion(e.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);

return reducedMotion;
}

// Usage
function AnimatedComponent() {
const reducedMotion = usePrefersReducedMotion();

return (
<motion.div
animate={{ x: 100 }}
transition={reducedMotion ? { duration: 0 } : { duration: 0.5 }}
>
Content
</motion.div>
);
}

Framer Motion Integration

import { useReducedMotion } from 'framer-motion';

function AnimatedCard({ children }) {
const shouldReduceMotion = useReducedMotion();

const variants = {
hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 20 },
visible: { opacity: 1, y: 0 },
};

return (
<motion.div
variants={variants}
initial="hidden"
animate="visible"
transition={{ duration: shouldReduceMotion ? 0 : 0.3 }}
>
{children}
</motion.div>
);
}

What to Disable vs. What to Keep

Animation TypeReduced Motion Action
Parallax scrollingRemove entirely
Auto-playing background videoPause or remove
Infinite spinning loadersReplace with pulse
Page transition slidesInstant transition (no slide)
Hover effects (scale, rotate)Remove or minimize
Progress bar fillKeep (essential feedback)
Loading skeleton shimmerReduce or remove shimmer
Accordion expandInstant open (no transition)

Real-World Impact

A website implementing reduced motion saw:

  • Bounce rate: 67% → 28% for users with motion sensitivity
  • Session duration: 45 seconds → 4.2 minutes

The accessibility improvement also measurably benefited business metrics.


Content from Frontend-Master-Prep-Series08-accessibility