prefers-reduced-motion Support
Disabling non-essential animation for users with vestibular sensitivity or preference.
AccessibilityBeginner
Why
Parallax, spinners, and slide-in animations can trigger nausea, dizziness, or seizures in vestibular-sensitive users. prefers-reduced-motion is the OS-level signal to dial it back — ignoring it is a WCAG 2.3.3 violation.
When
Always. Any animation longer than 5ms must respect prefers-reduced-motion. It's a 5-line CSS guard or a one-hook check — there's no excuse to skip it.
How
1. CSS guard — @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
2. JS hook — const reduce = useReducedMotion(); pass { duration: 0 } to framer-motion when true
3. Keep essential motion — a 150ms opacity fade for state change is fine; kill parallax, spin, bounce
4. Don't disable all — instant state changes (opacity 0→1) are fine; only kill the decorative motion
5. Test — toggle the OS setting (macOS System → Accessibility → Display → Reduce motion), verify
6. Library support — framer-motion useReducedMotion, GSAP matchMedia, CSS handles it natively
7. Announce — if motion carries information (progress bar), replace with a text or static indicator
Example
/* Global guard — kill decorative motion, keep instant state changes */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important; /* no smooth scroll */
}
}
// framer-motion — branch the transition
import { useReducedMotion } from "framer-motion";
function FadeIn({ children }: { children: React.ReactNode }) {
const reduce = useReducedMotion();
return (
<motion.div
initial={{ opacity: 0, y: reduce ? 0 : 8 }}
animate={{ opacity: 1, y: 0 }}
transition={reduce ? { duration: 0 } : { duration: 0.25, ease: "easeOut" }}
>
{children}
</motion.div>
);
}
Common Problems
- ⚠Killing all motion including opacity — state changes become invisible, users miss updates
- ⚠Only handling CSS, not JS animations — framer-motion parallax still spins for reduced-motion users
- ⚠No test — shipped without ever toggling the OS setting, broken motion ships to production
Best Practices
- ✓Keep instant opacity changes — only kill decorative motion (parallax, spin, bounce, slide)
- ✓Use a JS hook for animation libraries — framer-motion's useReducedMotion branches the transition
- ✓Test by toggling the OS setting — macOS Accessibility → Display → Reduce motion, verify every screen