Skip to content
brainNotFound

Build Systems/Rendering

Respecting prefers-reduced-motion, with a sandbox

The two-line media query, the resting-state rule that matters more, and a live example you can toggle yourself.

beginner10 min
On this page
  1. The rule that actually matters
  2. Try it
  3. The same idea in React

Most reduced-motion implementations stop at disabling transitions. The harder half is making sure the *resting* state is the visible one — because a reveal that starts at opacity: 0 and never animates is a blank page, not a calm one.

The rule that actually matters

Every animated element must be readable with no JavaScript and no animation. Test it by disabling JS entirely: if content disappears, the animation is load-bearing and needs inverting.

Try it

Toggle the checkbox to simulate the media query. Notice the second card stays readable throughout — that is the whole trick.

Edit the CSS and watch both cards
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <label class="toggle">
    <input type="checkbox" id="reduce" />
    Simulate prefers-reduced-motion
  </label>

  <div class="cards">
    <article class="card fragile">
      <h2>Fragile</h2>
      <p>Authored hidden. With motion off, this never appears.</p>
    </article>

    <article class="card robust">
      <h2>Robust</h2>
      <p>Authored visible. Motion is an enhancement, not a requirement.</p>
    </article>
  </div>

  <script src="/main.js"></script>
</body>
</html>

The same idea in React

Framer Motion's useReducedMotion returns the user's preference. The component below renders a plain element when motion is off — never a hidden one waiting for an animation that will not run.

A reveal that degrades correctly
import { m, LazyMotion, domAnimation, useReducedMotion } from "framer-motion";

function Reveal({ children }) {
  const reduced = useReducedMotion();

  // The resting state is the visible one. With motion off we render a plain
  // div — not a hidden one that happens not to animate.
  if (reduced) return <div className="card">{children}</div>;

  return (
    <m.div
      className="card"
      initial={{ opacity: 0, y: 16 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
    >
      {children}
    </m.div>
  );
}

export default function App() {
  return (
    <LazyMotion features={domAnimation} strict>
      <main>
        <h1>Reveal</h1>
        <Reveal>
          <p>
            Turn on "Reduce motion" in your OS settings and reload — this stays
            readable.
          </p>
        </Reveal>
      </main>
    </LazyMotion>
  );
}

// related

From the rest of the site.