ESC

Type to search the knowledge base.

prefers-reduced-motion

Honor prefers-reduced-motion — cut decorative animation, keep essential feedback, and wire CSS and JS motion to one preference.

beginner3 min read
  • css
  • prefers-reduced-motion
  • a11y

prefers-reduced-motion tells you the user asked for less motion — vestibular disorders, migraines, preference, or battery. Decorative parallax, auto-playing carousels, and looping logo spins should yield. Essential state changes can stay as instant or near-instant updates.

Docs: MDN prefers-reduced-motion, WCAG animation from interactions, web.dev reduced motion.

CSS kill switch (targeted)

.hero {
  animation: float 6s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .hero {
    animation: none;
  }

  .drawer {
    transition: none;
  }
}

Global nuclear option used by some resets:

@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;
  }
}

Nuclear works, but can break intentional opacity fades that aid comprehension — prefer layering reduced alternatives for product UI:

.toast {
  animation: slide-in 200ms ease-out;
}

@media (prefers-reduced-motion: reduce) {
  .toast {
    animation: fade-in 120ms linear;
  }
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

JavaScript must listen too

CSS alone won’t stop element.animate(), rAF loops, or Lottie.

const reduce = window.matchMedia("(prefers-reduced-motion: reduce)");

function scrollToId(id) {
  document.getElementById(id)?.scrollIntoView({
    behavior: reduce.matches ? "auto" : "smooth",
  });
}

reduce.addEventListener("change", () => {
  // tear down parallax listeners, pause videos, etc.
});

What to reduce vs keep

Reduce / remove Keep (usually)
Parallax, ken burns, large page transitions Instant show/hide of dialogs
Infinite decorative loops Progress indicators that don’t thrash position
Auto-advancing carousels User-initiated carousels with pause
Scroll-jacking Normal scrolling

If motion conveys meaning, provide a non-motion equivalent (text, icon state).

Scroll and view transitions

html {
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

Same for View Transitions API — skip or use a short crossfade only when reduced motion is off.

Interview out-loud

“prefers-reduced-motion: reduce means I strip or shorten decorative animation in CSS and disable smooth scrolling / WAAPI loops in JS. I listen to matchMedia change events. Essential feedback can remain as opacity or instant state. Ignoring this preference is an a11y bug in motion-heavy UIs.”

Footguns

  1. CSS-only fix while JS keeps animating.
  2. Removing all feedback so users can’t tell a click registered.
  3. Marketing pages with no pause on autoplay video backgrounds.
  4. Testing only with OS setting off.
  5. scroll-behavior: smooth on html without a reduce override.

Design system token

:root {
  --motion-distance: 0.5rem;
  --motion-duration: 200ms;
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --motion-distance: 0;
    --motion-duration: 0.01ms;
  }
}

.toast {
  transform: translateY(var(--motion-distance));
  transition: transform var(--motion-duration) ease, opacity var(--motion-duration) ease;
}

Centralizing duration/distance tokens keeps hundreds of components honest without pasting the media query everywhere. Still audit third-party widgets (carousels, Lottie, confetti) separately — they ignore your tokens.

QA steps

  1. Enable reduced motion in OS settings.
  2. Walk marketing home, modal open/close, route transitions, skeleton loaders.
  3. Confirm no infinite animations remain.
  4. Confirm focus and state changes still visible.
  5. Re-disable and ensure motion returns (matchMedia change handler).

Further reading

Related guides