# Guide: framer-motion-reduced-motion

Canonical: https://motionspec.dev/blog/framer-motion-reduced-motion

framer-motion and prefers-reduced-motion: what useReducedMotion does not cover

MotionSpec
← All guides

Guide

## framer-motion and prefers-reduced-motion: what useReducedMotion does not cover

Kevin Fröba · 2026-08-29 · MotionSpec

useReducedMotion() returns a boolean and changes nothing by itself. That is the single most common misreading of Motion for React's accessibility story: teams add the hook, see it in the diff, and consider the box ticked. The hook is a sensor, not a switch. The switch is <MotionConfig reducedMotion="user"> — and that one has a documented scope which is narrower than most people assume. This guide walks the four gaps that remain after you have done everything the library offers, with the pattern for each. If the media feature itself is new, start with prefers-reduced-motion, explained (https://motionspec.dev/blog/prefers-reduced-motion).

## What does useReducedMotion actually do?

It reports the preference. Nothing else. The Motion documentation is explicit:

"This hook returns true/false depending on whether your visitor has Reduced Motion enabled."
 — Motion for React, Accessibility (https://motion.dev/docs/react-accessibility)

```js
import { useReducedMotion } from "framer-motion"

function Card() {
  const shouldReduceMotion = useReducedMotion()

  return (
    <motion.div
      initial={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: 24 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: shouldReduceMotion ? 0.2 : 0.6 }}
    />
  )
}
```

Every branch in that snippet is yours to write. Ship the hook without the branches and the animation runs exactly as it did before — which is why "we use useReducedMotion" is not, on its own, an answer to "does this app honour reduced motion?".

## What does MotionConfig reducedMotion="user" cover?

It is the automatic path, and its scope is stated precisely in the docs:

"all motion components will automatically disable transform and layout animations, while preserving the animation of other values like opacity and backgroundColor."
 — Motion for React, Accessibility (https://motion.dev/docs/react-accessibility)

```js
import { MotionConfig } from "framer-motion"

// Wrap the app once. "user" follows the OS setting;
// "always" and "never" force the branch.
<MotionConfig reducedMotion="user">
  <App />
</MotionConfig>
```

Read that sentence twice, because both halves matter. All motion components — so anything that is not a motion component is untouched. And preserving opacity and backgroundColor — which is the correct design decision, not a bug: "reduce" means less movement, not a frozen page and never less information. It does mean that "reduced motion is on" and "nothing on screen is animating" are two different statements.

## The four gaps that remain

With MotionConfig wrapped around the whole app and every hook branch written, four categories of motion are still running. None of them is a flaw in the library; all four are routinely missed.

## 1. Every animation that is not a motion component

A real application is a mixture. The hero is a motion.div; the skeleton loader is a Tailwind animate-pulse (https://motionspec.dev/blog/tailwind-animate-wcag-2-2-2); the notification badge is a hand-written CSS keyframe; the testimonial strip is a third-party carousel with its own stylesheet. MotionConfig reaches the first of those. The rest need the CSS rail:

```css
/* The safety net for everything MotionConfig cannot reach. */
@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;
  }
}
```

This is a blunt instrument and it is meant to be — it is the floor, not the design. Where a specific animation carries information, give it a hand-written reduced variant instead of letting the blanket rule flatten it.

## 2. Colour and opacity loops, which are preserved by design

Because reducedMotion="user" keeps animating opacity and backgroundColor, a motion component set to pulse its background forever keeps pulsing under reduced motion. For a slow fade that is exactly right. For a high-contrast colour flash it is not — and flashing content is governed by a different criterion (WCAG 2.3.1) that has nothing to do with the preference. Audit your infinite loops by hand:

```js
// This keeps running under reducedMotion="user":
<motion.div
  animate={{ backgroundColor: ["#0ea5e9", "#f43f5e"] }}
  transition={{ repeat: Infinity, duration: 0.8 }}
/>

// Branch it explicitly instead.
const shouldReduceMotion = useReducedMotion()
<motion.div
  animate={shouldReduceMotion ? { backgroundColor: "#0ea5e9" } : { backgroundColor: ["#0ea5e9", "#f43f5e"] }}
  transition={shouldReduceMotion ? { duration: 0 } : { repeat: Infinity, duration: 0.8 }}
/>
```

## 3. WCAG 2.2.2, which neither API addresses

This is the gap with the highest stakes, because 2.2.2 is Level A. Both useReducedMotion and MotionConfig read an operating-system preference, which maps to WCAG 2.3.3 (https://motionspec.dev/blog/wcag-2-3-3-animation-from-interactions) — Level AAA, a best-practice track. WCAG 2.2.2 Pause, Stop, Hide (https://motionspec.dev/blog/wcag-2-2-2-pause-stop-hide) is the Level A criterion, and it asks for something no preference can provide: a mechanism on the page that any user can operate. A motion component with repeat: Infinity runs forever for every visitor who has never opened that OS setting, no matter what MotionConfig is set to.

The fix is a control, and in React it is small — a context flag that every looping animation reads:

```js
const [paused, setPaused] = useState(false)

<button type="button" aria-pressed={paused} onClick={() => setPaused(p => !p)}>
  {paused ? "Resume animation" : "Pause animation"}
</button>

<motion.div
  animate={paused ? false : { rotate: 360 }}
  transition={{ repeat: Infinity, duration: 2, ease: "linear" }}
/>
```

Setting animate to false stops the component driving new values; the element stays where it is. The control has to be visible and keyboard-operable — a hover-pause does not satisfy the criterion, because a keyboard user never triggers it.

## 4. Autoplaying media and embeds

An autoplaying background video, a looping GIF, an embedded third-party widget: none of them is a motion component and none of them reads your MotionConfig. Video needs the preference checked in your own code and a real pause control if it runs longer than five seconds alongside other content.

## What a CSS scan can and cannot tell you here

This is the part worth being blunt about, because it cuts against our own product. On 2026-08-29 we ran our own free motion check (https://motionspec.dev/motion-check) against https://motionspec.dev/. Result: score 100/100, zero findings — alongside this disclosure, printed with the result:

```
"disclosures": [
  "Runtime motion detected (Element.animate() (Web Animations API)): not audited (V2)."
]
```

That is the honest shape of a static CSS scan. Motion for React drives animation from JavaScript, largely through the Web Animations API; a scanner that reads linked stylesheets and inline <style> blocks cannot see a single frame of it. A clean result on a Motion-heavy app means clean on the CSS it could read — nothing more. The same limit applies to GSAP (https://motionspec.dev/blog/gsap-prefers-reduced-motion), scroll-driven libraries, and anything else animating from script.

## A checklist you can run in ten minutes

Wrap the app in <MotionConfig reducedMotion="user">. One line, covers the transform and layout half.

Grep for repeat: Infinity. Every hit is a WCAG 2.2.2 candidate — either bound it or give it a pause control.

Grep for backgroundColor and opacity in looping transitions. These survive reducedMotion="user" by design; decide each one deliberately.

Add the CSS safety net for the animation that is not a motion component.

Check the route transitions separately. If the app uses the React ViewTransition component, its motion lives on the ::view-transition pseudo-elements and needs its own guard — see Next.js View Transitions and reduced motion (https://motionspec.dev/blog/nextjs-view-transitions-reduced-motion).

Toggle the OS setting and reload. macOS: System Settings → Accessibility → Display → Reduce motion. Then use the page for a minute. The full test procedure is in how to verify web motion accessibility (https://motionspec.dev/blog/verify-web-motion-accessibility).

Scope note: MotionSpec's static scan does not read Motion for React.

The free motion check (https://motionspec.dev/motion-check) reads a page's linked CSS files and inline style blocks — nothing else. Animation driven by JavaScript, including Motion for React, GSAP and the Web Animations API, is out of scope, and the scan says so in its disclosures when it detects a runtime motion source. For motion you generate rather than audit, the MotionSpec compiler ships the reduced-motion branch by construction — see the MCP docs (https://motionspec.dev/docs), or MotionSpec for developers (https://motionspec.dev/for/developers).

## FAQ

## Does useReducedMotion disable animations automatically?

No. The hook returns true or false depending on whether the visitor has Reduced Motion enabled. It changes nothing on its own — you branch on the value yourself. The automatic behaviour lives in MotionConfig with reducedMotion="user".

## What does MotionConfig reducedMotion="user" actually disable?

Per the Motion documentation, all motion components automatically disable transform and layout animations, while preserving the animation of other values like opacity and backgroundColor. That is deliberate: reduced motion means less movement, not less information. It also means a colour or opacity loop keeps running.

## Does Motion for React handle WCAG 2.2.2 Pause, Stop, Hide?

No. Both useReducedMotion and MotionConfig read an operating-system preference, which maps to WCAG 2.3.3 (Level AAA). WCAG 2.2.2 (Level A) asks for an on-page mechanism any user can operate. An animation with repeat: Infinity keeps running for every visitor who never set the OS preference, whatever MotionConfig is set to.

## Can a CSS scan detect unguarded Motion for React animations?

No. Motion for React drives animation from JavaScript, largely through the Web Animations API, so a static CSS scan cannot see it. A clean scan result on a Motion-heavy app means clean on the CSS it could read, not that the JavaScript motion was checked.

Check the CSS half in ten seconds.

The free motion check reads static CSS only — your Motion for React code stays outside its scope, and the result says so — but every CSS animation and transition on the page is scanned against WCAG 2.2.2 and 2.3.3, with a fix for each finding. No signup, nothing stored.

Run the free motion check

Generating web UI at scale? See the Design Partner Program. (https://motionspec.dev/#en-program)

MotionSpec checks and compiles UI animation for AI-generated web apps — it does not generate AI video.

See a real example report → https://motionspec.dev/report-example (Markdown: https://motionspec.dev/md/report-example.md) — a complete report from a real scan of our own /before-after demo page, not a mock-up.

MotionSpec All guides Free motion check Verify motion accessibility MCP docs Pricing Impressum Datenschutz

MotionSpec enforces reduced-motion safety and a performance budget for the motion it compiles; it is not a general accessibility guarantee and does not claim conformance with any specific accessibility law. © 2026 Fröba Sales Solutions UG (haftungsbeschränkt).
