Technical note

The accessibility setting that breaks React hydration

We had animated reveals on a client build. framer-motion, Next.js, server rendering. The pattern looked responsible: call useReducedMotion(), and if the visitor has Reduce Motion turned on, return a plain div instead of a motion.div.

Respect the setting, skip the animation. Textbook.

Then an automated screenshot pass ran with prefers-reduced-motion: reduce emulated, and the console filled with hydration errors. React was throwing away server markup and rebuilding it on the client. And it was doing that for exactly the visitors the code was trying to be kind to.

Why the server always answers wrong

useReducedMotion() reads a media query. The server has no media query to read. It cannot see the visitor's OS settings, so on the server the hook returns false every time, and the server renders the motion markup every time.

Now a visitor with Reduce Motion enabled loads the page. Their browser hydrates the component, the hook returns true, and the client renders the plain branch.

Server said motion.div. Client said div. React reports a mismatch and re-renders the tree.

Reduce Motion is not an edge case. Plenty of people turn it on because parallax and slide-ins make them genuinely ill. This bug ships silently and fires only on the machines of the people it was written to help.

The mounted gate

The fix is a wrapper hook. Call it useReducedMotionSafe(). It returns false during server rendering and the first client render, then the real preference after that.

The mechanics: hold a mounted boolean in state, flip it to true inside a useEffect, and return the real useReducedMotion() value only once mounted is true. Effects never run on the server, so the server and the first client render both take the motion branch. The markup matches, hydration passes, and one render later the reduced-motion branch takes over.

The visitor with Reduce Motion on still gets a static page. They just get it without React tearing the DOM down first.

Where the raw hook is still fine

Hydration only cares about what the server rendered: DOM structure and initial styles. That draws a clean line.

Reading useReducedMotion() inside an event handler is safe. Using it only in framer-motion's animate or transition props is safe. Both run after hydration and never touch server HTML. The trap is branching the rendered structure, or the initial styles, on a value the server cannot know.

Finding it before your visitors do

Nothing flagged this for us. Lighthouse passed. Visual passes passed. It surfaced because a screenshot run happened to emulate the reduced-motion media query with the console open.

So that is the test: open devtools, emulate prefers-reduced-motion: reduce, hard reload, read the console. Takes a minute per page. We shipped this pattern on more than one site before we caught it, so every other framer-motion site we run is now on the audit list.

If you have a server-rendered React site with framer-motion animations, odds are decent this bug is sitting in it right now. The gate is a dozen lines. The folks it protects will never know you wrote it, and that is the good kind of fix.