Skip to content
Back to Design Hub

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

FAQ