A practical guide to ‘prefers-reduced-motion’ in React and Next.js, from CSS fallbacks to Motion, GSAP, page transitions, scroll effects and WebGL.
A panel slides 600 pixels across the screen. A route change wipes the viewport. A background keeps drifting behind the copy. Then someone adds:
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
}
}
Technically, the page now has a reduced-motion rule. Practically, nobody has decided what any of those interactions should become.
prefers-reduced-motion: reduce does not necessarily mean removing every transition from an interface. It means non-essential motion should be minimized, removed, or replaced. The useful production question is whether each interaction should disappear, become a calmer state change, or retain limited feedback because that feedback still carries information.
Detecting the preference is the easy part. Designing the quieter version is where the work starts.
What ‘prefers-reduced-motion’ actually tells you
The CSS media feature has two values: no-preference and reduce.
When reduce matches, the user has enabled a device or operating-system preference asking interfaces to minimize non-essential motion. MDN describes responses broadly: motion can be removed, reduced, or replaced, with large scaling and panning movements among the patterns that can cause problems for people with vestibular motion sensitivity.
The browser does not rewrite your animation system for you.
That distinction matters in React because the same preference can lead to different implementations.
A drawer that normally moves across half the viewport might stop translating and appear through a short opacity change instead.
Decorative parallax behind a hero might simply disappear.
A loader may still need to communicate that work is happening, even if the more decorative looping animation is removed.
Same preference. Different job.
React's View Transition documentation makes a similar implementation point: React does not automatically remove those animations for users who request reduced motion. The application still needs to decide how the transition should behave.
Start with the motion job, not the animation library
It is easy to frame reduced motion as a GSAP problem, a Motion problem, or a CSS problem.
It is usually a product-state problem first.
Ask what the movement contributes before deciding how to rewrite it.
A useful model is:
Decision | Use it when | Example |
Remove | The movement is decorative, continuous, spatially large, or unnecessary to the task | Parallax, cursor trails, ambient camera drift |
Replace | The state change remains useful but the spatial movement does not | Sliding drawer becomes a short opacity change |
Retain carefully | The feedback communicates information that would otherwise become harder to understand | Restrained loading or status feedback |
W3C's guidance makes a similar functional distinction. WCAG 2.2 Success Criterion 2.3.3 covers non-essential motion animation triggered by interaction and allows essential animation where removing it would fundamentally change the information or functionality.
That gives you a better starting question than “How do I turn animation off?”
Ask:
What breaks if this movement disappears?
If the answer is nothing, remove it.
If the interface still needs to communicate a state change, replace the movement with something quieter.
If the movement is carrying information, preserve the information while reducing the unnecessary motion around it.
Use CSS first when CSS owns the behavior
If CSS controls the animation, CSS should usually control its reduced state too.
The browser can resolve the media query before React effects start running, which is particularly useful for entrance animations and other first-paint behavior.
Consider a panel that normally moves upward:
.panel {
opacity: 0;
transform: translateY(24px);
transition:
opacity 180ms ease,
transform 420ms cubic-bezier(.2, .8, .2, 1);
}
.panel[data-open="true"] {
opacity: 1;
transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
.panel {
transform: none;
transition: opacity 120ms linear;
}
}
The reduced version is not the same movement played faster. The spatial travel is gone.
For motion-heavy interfaces, it can be even cleaner to treat reduced behavior as the baseline and opt into movement only when the user has expressed no preference against it:
.card {
opacity: 1;
transform: none;
}
@media (prefers-reduced-motion: no-preference) {
.card {
opacity: 0;
transform: translateY(24px);
transition:
opacity 180ms ease,
transform 420ms cubic-bezier(.2, .8, .2, 1);
}
.card[data-visible="true"] {
opacity: 1;
transform: translateY(0);
}
}
That approach is especially useful when an element animates during initial rendering. The safe state exists before JavaScript is available.
A global 0.01ms reset can still be an intentional site policy, but it is a blunt instrument. It treats a route transition, tooltip, progress indicator and decorative background as though they serve the same purpose.
They do not.
Vault's dependency guidance similarly recommends scoped reduced-motion decisions rather than assuming one global override correctly represents every interaction.
Use JavaScript when the behavior itself needs to change
CSS can change transforms, opacity, durations and keyframes.
Sometimes the reduced state needs to alter the component's behavior instead.
A cursor effect might stop creating particles on pointermove. A carousel might stop autoplay. A WebGL scene might avoid starting continuous camera movement. A scroll animation might skip the animation timeline and render the final content state immediately.
For those cases, move the preference into application logic.
In React and Next.js, an SSR-safe hook should avoid assuming full motion before the browser has had a chance to report the user's preference.
One option is useSyncExternalStore:
"use client";
import { useSyncExternalStore } from "react";
const query = "(prefers-reduced-motion: reduce)";
function subscribe(callback: () => void) {
const media = window.matchMedia(query);
media.addEventListener("change", callback);
return () => {
media.removeEventListener("change", callback);
};
}
function getSnapshot() {
return window.matchMedia(query).matches;
}
function getServerSnapshot() {
// The server cannot know the user's OS preference.
// Default to the quieter state until the client can check.
return true;
}
export function usePrefersReducedMotion() {
return useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot
);
}
The important decision is getServerSnapshot().
The server cannot inspect the user's operating-system preference, so this version defaults to the quieter state. Once the component hydrates, React can read the real media-query result and update accordingly.
That is preferable to initializing the state as false, which can briefly treat a user who requested reduced motion as though full motion were allowed.
It also listens for changes. If the user changes the preference while the page is open, the component can react without requiring a reload.
CSS should still handle presentation-level first-paint behavior where possible. JavaScript belongs here when the behavior, not merely the styles, needs a different branch.
Motion: central policy or per-component decisions
If the project already uses Motion, you do not need to build a custom media-query abstraction solely to read the preference.
For a broad application policy, Motion provides MotionConfig:
import { MotionConfig } from "motion/react";
export function MotionProvider({
children,
}: {
children: React.ReactNode;
}) {
return (
<MotionConfig reducedMotion="user">
{children}
</MotionConfig>
);
}
With reducedMotion="user", Motion respects the user's reduced-motion preference across descendant Motion components.
This is useful when the application has a consistent policy. Transform and layout movement can be suppressed while calmer properties such as opacity can still be used where appropriate.
Sometimes a component needs a more specific fallback. That is where useReducedMotion() becomes useful:
import {
motion,
useReducedMotion,
} from "motion/react";
export function Drawer({
open,
}: {
open: boolean;
}) {
const reduceMotion = useReducedMotion();
return (
<motion.aside
animate={{
opacity: open ? 1 : 0,
x: reduceMotion
? 0
: open
? 0
: -48,
}}
transition={{
duration: reduceMotion ? 0.12 : 0.4,
}}
/>
);
}
Here the reduced version changes the animation model. The drawer no longer travels horizontally.
Use MotionConfig when the project needs a consistent policy. Use useReducedMotion() when the component needs to make a specific behavioral or visual decision.
GSAP: branch the timeline instead of shrinking it
GSAP provides gsap.matchMedia(), which can organize animation logic around media conditions including prefers-reduced-motion.
In React, keep browser-dependent GSAP behavior inside a client component and scope selectors or refs to the component rather than relying on document-wide targets.
"use client";
import { useLayoutEffect, useRef } from "react";
import gsap from "gsap";
export function HeroCopy() {
const root = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const mm = gsap.matchMedia();
mm.add(
{
reduce:
"(prefers-reduced-motion: reduce)",
full:
"(prefers-reduced-motion: no-preference)",
},
(context) => {
const { reduce } = context.conditions!;
const animation = gsap.context(() => {
gsap.fromTo(
"[data-hero-copy]",
{
opacity: 0,
y: reduce ? 0 : 48,
},
{
opacity: 1,
y: 0,
duration: reduce ? 0.15 : 0.65,
}
);
}, root);
return () => animation.revert();
}
);
return () => mm.revert();
}, []);
return (
<div ref={root}>
<div data-hero-copy>
Your hero content
</div>
</div>
);
}
Again, the useful part is not that the duration becomes smaller.
The large positional movement disappears.
Use the animation system that already owns the behavior. What matters is a clear reduced state and a cleanup path that survives component lifecycle changes.
What reduced motion should look like across interaction types
One reduced-motion rule cannot sensibly cover every creative frontend pattern.
Interaction | Full-motion version | Reduced version | Production check |
Scroll / parallax | Elements translate, scale or move at different scroll rates | Normal document flow, immediate reveals, or minimal opacity changes | Keep logical DOM order and make all content reachable without the animation timeline |
Custom cursor | Delayed follower, particles, trails or distortion | Native cursor; remove follower/trail | Preserve native click and focus behavior; handle touch/coarse pointers separately |
Page transition | Full-screen wipe, masks, panels, route choreography | Immediate navigation or restrained fade | Navigation state remains real; restore focus after route change |
WebGL / 3D | Camera drift, distortion, particles, continuous render loop | Static scene, poster image, limited state change, or conventional HTML fallback | Stop unnecessary render work as well as visible movement |
Loader | Looping numbers, shapes or spatial sequences | Static or restrained progress/status treatment | Keep real waiting-state information and relevant status semantics |
Text animation | Scramble, translate, stagger or character reveal | Full readable text immediately or brief opacity change | Never make essential body, form or error content depend on animation |
Carousel | Spatial slide movement or shader transition | Instant slide update or short dissolve | Keep controls, active state and keyboard behavior intact |
This is the part global resets cannot decide for you.
The reduced state should preserve the interaction's job. It does not have to preserve its spectacle.
What this looks like in Vault effects
Source access matters here for a practical reason: reduced-motion behavior often needs to change timing, triggers, render loops, route handling or the animation model itself.
Those choices are easier to inspect when the implementation lives in your project.
Consider Vault's Block Transition. Its normal behavior creates a larger visual route-change moment with blocks or panels covering and revealing the page. Its reduced-motion path uses immediate navigation or a restrained fade rather than preserving the full-screen choreography.
The route still changes. The large movement does not need to.
WebGL Slider needs a different fallback. Its regular interaction uses shader-led distortion between slides. Reduced motion can remove the distortion and depth while preserving the underlying carousel through static slides, instant changes or a short fade.
The shader is negotiable. Readable controls, slide state and usable content are not.
This is why effect-level verification matters. Vault effects can use CSS, GSAP, Motion, WebGL, Three.js or other technologies depending on the interaction. A CSS text effect and a shader-driven carousel should not receive identical production advice just because both happen to move.
Vault's source-first workflow gives you editable implementation files inside the project. It does not remove the need to decide what the reduced state should be.
‘prefers-reduced-motion’ is not a WCAG switch
Good reduced-motion handling and broad accessibility compliance are not interchangeable claims.
WCAG 2.2 Success Criterion 2.3.3, Animation from Interactions, is Level AAA. It concerns non-essential motion animation triggered by interaction and allows animation that is essential to the functionality or information being conveyed.
W3C lists prefers-reduced-motion approaches among sufficient techniques, but using the media query does not certify an entire component or site.
WCAG 2.2 Success Criterion 2.2.2, Pause, Stop, Hide, is Level A and addresses certain automatically moving, blinking, scrolling or updating content.
For qualifying automatically moving content that starts without user action, lasts longer than five seconds and appears alongside other content, users need a mechanism to pause, stop or hide it unless the movement is essential.
Those are different requirements.
Reduced motion should sit alongside keyboard operation, focus management, semantic structure, readable content, autoplay controls and appropriate touch behavior. It should not be used as shorthand for “accessible.”
Test reduced motion as a real product state
Detecting the media query proves almost nothing about the resulting interface.
Test the state.
1. Emulate the preference in browser DevTools
In Chrome DevTools, open the Rendering panel and emulate the CSS media feature:
prefers-reduced-motion: reduce
Then navigate through the page normally.
Look for:
entrance animations that still run;
elements that remain hidden because their reveal timeline was skipped;
route overlays that never clear;
autoplay that continues despite reduced movement;
canvases that look static but still render continuously.
2. Test with the actual operating-system preference
Browser emulation is useful for development. It should not be your only check.
Enable reduced motion at the operating-system level and use the site as a normal visitor would.
This catches integration differences that isolated component testing may miss.
3. Change the preference while the page is open
A JavaScript implementation should respond if the underlying media query changes.
Toggle the preference without refreshing.
If the site only reads matchMedia().matches once at startup, it may miss this case.
4. Check first paint and hydration
Reload a server-rendered page with reduced motion enabled.
Watch for a brief full-motion entrance before React hydrates.
Also check the browser console for hydration warnings. The reduced state should not require the server to guess a value that produces incompatible markup.
5. Test the interaction without its animation
A route transition should still restore focus correctly.
A scroll story should still expose the complete content.
A carousel should still announce and expose its current state.
A cursor effect should leave normal clicking intact.
A WebGL fallback should not hide meaningful copy inside a canvas.
6. Add a regression test where the behavior matters
Playwright can emulate the preference:
import { test, expect } from "@playwright/test";
test("uses the reduced-motion state", async ({
page,
}) => {
await page.emulateMedia({
reducedMotion: "reduce",
});
await page.goto("/");
const hero = page.locator(
"[data-testid='hero-copy']"
);
await expect(hero).toHaveCSS(
"transform",
"none"
);
});
The exact assertion depends on the effect. For some components the correct test is that autoplay does not start, a WebGL loop is not mounted, or the final content state is immediately visible.
The point is to test the behavior, not merely the existence of a media query in the stylesheet.
Treat reduced motion as an explicit component state
For React and Next.js teams, reduced motion deserves the same implementation attention as a loading state or responsive breakpoint.
Design it.
Implement it.
Review it.
Test it with the full page rather than only inside an effect demo.
Sometimes the correct result will be static. Sometimes large spatial motion becomes opacity. Sometimes the architecture changes completely: WebGL becomes a poster image, parallax becomes normal scrolling, a cursor trail disappears, or a route transition becomes immediate navigation.
The useful question is not:
How much animation can we keep?
It is:
What is the calmest version of this interaction that still does its job?
When you want an editable starting point for that interaction layer, browse the Hyperiux Vault effects. Preview the effect, inspect the source, then design the reduced state with the same care as the version that moves.
FAQ
Does ‘prefers-reduced-motion: reduce’ mean no animation?
No. It signals that the user prefers less non-essential motion. Large spatial movement, parallax and continuous decorative effects often need to be removed, while some state changes can use an instant update or restrained alternative such as opacity. The appropriate fallback depends on what the motion is communicating.
Should opacity transitions remain for reduced motion?
Sometimes. Replacing large translations or scale changes with a short opacity transition can preserve visual continuity with much less spatial movement. It should not be treated as a universal rule, though. If the fade adds no useful feedback, an immediate state change may be better.
Should I handle reduced motion in CSS or React?
Use CSS when CSS owns the behavior. It works before client JavaScript and is particularly useful for first-paint animation.
Use React or JavaScript when the application's behavior needs to change, such as disabling autoplay, avoiding a render loop, changing an animation timeline, or selecting a different component state.
How should I handle reduced motion with Motion?
For a project-wide policy, Motion provides MotionConfig with reducedMotion="user". For component-specific decisions, useReducedMotion() lets the component replace or remove particular animation behavior.
Does supporting ‘prefers-reduced-motion’ make a site WCAG compliant?
No. It can be part of satisfying relevant animation requirements, but accessibility also involves factors such as keyboard operation, focus management, semantics, readable content and controls for applicable automatically moving content. Compliance requires evaluating the defined WCAG criteria that apply to the interface.