Skip to content

Accessibility and Motion

When to Use

Apply reduced-motion handling to every animation pattern. WCAG 2.1 SC 2.3.3 requires it. Replace motion with crossfade — do not use a kill switch that removes all feedback.

Decision

Situation Choose Why
Reduced motion for all animations @media (prefers-reduced-motion: reduce) System-level user preference
Alternative to killing all motion Replace transform-based with opacity crossfade Users still get state feedback without spatial motion
Keyboard focus indicator :focus-visible with 2px 3:1 contrast ring Only shows for keyboard nav, not mouse clicks
High contrast mode support @media (forced-colors: active) Windows High Contrast, some assistive tech
JS-side motion detection matchMedia('(prefers-reduced-motion: reduce)') Skip IntersectionObserver animations

Pattern

Option A — Kill switch (quick but removes all feedback):

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Option B (preferred) — Crossfade replacement:

@media (prefers-reduced-motion: reduce) {
  .reveal { transform: none; transition: opacity var(--duration-moderate) var(--ease-standard); }
  .card:hover { transform: none; filter: brightness(1.05); }
  .scroll-reveal { animation: none; opacity: 1; transform: none; }
}

JavaScript detection:

const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
if (prefersReducedMotion.matches) { /* skip animations */ }
prefersReducedMotion.addEventListener('change', (e) => {
  document.documentElement.classList.toggle('reduce-motion', e.matches);
});

Focus-visible with forced-colors fallback:

:where(a, button, input, [tabindex]):focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--color-background), 0 0 0 4px var(--color-focus, currentColor);
}
@media (forced-colors: active) {
  :focus-visible { outline: 2px solid LinkText; outline-offset: 2px; box-shadow: none; }
}

WCAG Checklist

Requirement WCAG Rule
prefers-reduced-motion 2.3.3 Replace spatial motion with crossfade, not removal
Focus indicator contrast 1.4.11 3:1 minimum, 2px minimum thickness
Auto-playing animation >5s 2.2.2 Must have pause/stop controls
Motion as sole state indicator 1.3.3 Never — pair with color, icon, or text
Hover content keyboard accessible 1.4.13 Match :hover with :focus-visible
Forced colors 1.4.11 Functional indicators must work in forced-colors: active
Flashing content 2.3.1 No more than 3 flashes per second

Common Mistakes

  • Wrong: prefers-reduced-motion as kill switch only — Right: Replace with crossfade; users lose all feedback with kill switch
  • Wrong: Focus ring invisible on dark backgrounds — Right: Test on both light and dark themes
  • Wrong: box-shadow focus ring without forced-colors fallback — Right: box-shadow is invisible in Windows High Contrast mode
  • Wrong: Hover-only interactions — Right: Always add keyboard equivalents for screen reader and keyboard users
  • Wrong: * { outline: none } globally — Right: Use :focus:not(:focus-visible) to hide outline for mouse only

See Also