Skip to main content
Accessible animations respect user preferences and ensure your content works for everyone, including users with vestibular disorders, seizure disorders, or those using assistive technologies.

Respecting motion preferences

Many users enable “Reduce Motion” in their operating system settings due to vestibular disorders or motion sensitivity.

CSS approach

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

JavaScript detection

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

if (prefersReducedMotion) {
  // Use instant transitions or simpler animations
}

React implementation

The Animation Playground includes an accessibility control:
function AccessibilityControls() {
  const [prefersReducedMotion, setPrefersReducedMotion] = useState(false)

  return (
    <div className="flex items-center gap-4 p-4 bg-yellow-50 rounded-lg">
      <label className="flex items-center gap-2">
        <input
          type="checkbox"
          checked={prefersReducedMotion}
          onChange={(e) => setPrefersReducedMotion(e.target.checked)}
        />
        <span className="text-sm">Simulate prefers-reduced-motion</span>
      </label>
      <InfoTooltip
        title="Accessibility"
        content="Users can enable reduced motion in their OS settings. Your animations should respect this preference."
      />
    </div>
  )
}

Framer Motion support

import { motion, useReducedMotion } from 'framer-motion'

function Component() {
  const shouldReduceMotion = useReducedMotion()

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

Safe animation practices

Avoid flashing

Never flash more than 3 times per second - can trigger seizures

Limit parallax

Excessive parallax scrolling can cause nausea

Provide pause controls

For auto-playing animations, include pause/play controls

Keep animations subtle

Large, fast movements are more disorienting

ARIA considerations

Animated content announcements

<div role="status" aria-live="polite" aria-atomic="true">
  {isAnimating ? 'Loading...' : 'Content loaded'}
</div>

Focus management

<motion.div
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  onAnimationComplete={() => {
    // Focus the first interactive element when animation completes
    firstInputRef.current?.focus()
  }}
>
  <input ref={firstInputRef} />
</motion.div>

Testing checklist

1

Enable reduced motion

Test with OS-level reduced motion enabled:
  • macOS: System Preferences → Accessibility → Display → Reduce motion
  • Windows: Settings → Ease of Access → Display → Show animations
  • iOS/Android: Settings → Accessibility → Reduce motion
2

Use keyboard navigation

Ensure all interactive elements are reachable and usable with keyboard only. Animations shouldn’t interfere with Tab order.
3

Test with screen readers

Verify that screen readers correctly announce state changes triggered by animations.
4

Check color contrast

Animated elements should maintain WCAG AA contrast ratios (4.5:1 for text).

Implementation patterns

Conditional animations

const variants = {
  initial: { opacity: 0, y: 20 },
  animate: { 
    opacity: 1, 
    y: 0,
    transition: {
      duration: prefersReducedMotion ? 0 : 0.5,
      ease: 'easeOut'
    }
  }
}

Fallback to crossfade

const transition = prefersReducedMotion
  ? { duration: 0.2, ease: 'linear' }  // Quick crossfade
  : { duration: 0.5, ease: 'easeInOut', type: 'spring' }  // Full animation
Don’t completely remove animations for users with motion preferences. Use simpler, quicker transitions instead of instant changes.

Resources

Build docs developers (and LLMs) love