All guides

How to Respect prefers-reduced-motion in React

By Hizmedia · · 6 min read

Some people turn on reduced motion because large movement on screen makes them dizzy or sick. Others have migraines, vestibular disorders or simply find animation distracting. Every major operating system has the setting, and browsers pass it to your site. Respecting it is a small amount of code, and it's the difference between a site that feels polished and one that makes some visitors close the tab.

Where the setting lives

People switch it on in their operating system, not in your site, and the browser exposes it as the prefers-reduced-motion media feature:

  • macOS: System Settings, Accessibility, Display, Reduce motion
  • iOS and iPadOS: Settings, Accessibility, Motion, Reduce Motion
  • Windows 11: Settings, Accessibility, Visual effects, Animation effects (off)
  • Android: Settings, Accessibility, Remove animations (the wording varies by maker)

It has two values: no-preference and reduce. There's nothing to ask for and nothing to store: read it, and adapt.

In CSS

For CSS animations and transitions, the media query is all you need. Tailwind's motion-reduce: and motion-safe: variants are the same query in class form.

globals.css
.marquee-track {
  animation: scroll 30s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  .marquee-track {
    animation: none;
  }
}
example.tsx
<div class="animate-spin motion-reduce:animate-none">…</div>

In React with Motion

Motion's useReducedMotion hook returns true when the setting is on, so you can choose different values per component. It updates live if someone changes the setting while your page is open.

card.tsx
"use client";

import { motion, useReducedMotion } from "motion/react";

export function Card({ children }: { children: React.ReactNode }) {
  const reduceMotion = useReducedMotion();
  return (
    <motion.div
      // with reduced motion the card fades in; otherwise it also rises
      initial={{ opacity: 0, y: reduceMotion ? 0 : 24 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true }}
    >
      {children}
    </motion.div>
  );
}

To set a default for a whole app, wrap it in MotionConfig with reducedMotion="user". Motion then skips transform and layout animations for people with the setting on, while keeping opacity and colour changes, which are usually safe.

providers.tsx
"use client";

import { MotionConfig } from "motion/react";

export function Providers({ children }: { children: React.ReactNode }) {
  return <MotionConfig reducedMotion="user">{children}</MotionConfig>;
}

Canvas and requestAnimationFrame loops don't go through Motion, so check the media query yourself with window.matchMedia("(prefers-reduced-motion: reduce)") and draw a single still frame instead of starting the loop.

What to remove and what to keep

Reduced motion doesn't mean no feedback. The aim is to remove movement that travels, spins, zooms or parallaxes, and keep the information the animation carried.

  • Remove: parallax, large slides and zooms, spinning, auto-playing loops, scroll-linked movement, elements that follow the cursor.
  • Keep: short opacity fades, colour changes, focus rings, progress that fills in place, a tick that appears when a form saves.
  • Show the end state: text that would have typed itself appears whole, a number that would have counted up shows its final value, a chart that would have drawn appears drawn.
  • Never hide meaning behind motion: if a status was only shown by a shake, also show it in words.

Every MotionKit component follows these rules, and each component page has an accessibility report saying exactly what changes with reduced motion on.

Loops need a pause, whatever the setting

WCAG 2.2 success criterion 2.2.2 (Pause, Stop, Hide, level A) applies to everyone, not only people with reduced motion on: anything that moves on its own for more than five seconds, alongside other content, needs a way to pause, stop or hide it. A pause on hover or focus helps, but a visible pause button is the safest answer for marquees, carousels and ambient backgrounds behind text.

How to test it

You don't need to change your system setting to test. In Chrome or Edge, open DevTools, press Command+Shift+P (Ctrl+Shift+P on Windows), type "Rendering", open the Rendering panel and set "Emulate CSS media feature prefers-reduced-motion" to "reduce". Firefox and Safari follow the system setting. Then click through every animated part of the page and ask: did anything still travel, spin or zoom, and did I lose any information?