usePrefersReducedMotion
A React hook for detecting user motion preferences via the prefers-reduced-motion media query, optimized for performance and SSR safety using useSyncExternalStore.
Introduction
The usePrefersReducedMotion hook allows your application to respect a user's operating system settings for reduced motion. By detecting this preference, you can programmatically disable heavy animations or transitions, making your site more accessible to users with vestibular disorders or motion sensitivities.
Basic Usage
import { usePrefersReducedMotion } from '@/hooks/usePrefersReducedMotion';
function HeroSection() {
const prefersReducedMotion = usePrefersReducedMotion();
return (
<div className={prefersReducedMotion ? 'fade-in' : 'slide-and-zoom'}>
<h1>Welcome to Hookipedia</h1>
</div>
);
}API Reference
Parameters
Prop
Type
Returns
boolean
Returns true if the user has requested the system to minimize the amount of non-essential motion.
Hook
import { useSyncExternalStore } from 'react';
const REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)';
const getMql = () =>
typeof window === 'undefined'
? null
: window.matchMedia(REDUCED_MOTION_QUERY);
/**
* Global subscriber for the media query change event.
*/
function subscribe(callback: () => void): () => void {
const mql = getMql();
if (!mql) {
return () => {}; // NOOP for server-side
}
mql.addEventListener('change', callback);
return () => {
mql.removeEventListener('change', callback);
};
}
/**
* Hook to detect user preference for reduced motion.
* * @param serverValue - The value used during SSR/Hydration.
* Defaults to 'true' (safety first) or 'false' based on your UI strategy.
*/
export function usePrefersReducedMotion(serverValue: boolean = false): boolean {
// Snapshot returns the current state of the media query
const getSnapshot = (): boolean => {
return getMql()?.matches ?? serverValue;
};
// Server snapshot returns the fixed value provided during SSR
const getServerSnapshot = (): boolean => {
return serverValue;
};
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}Advanced Examples
Framer Motion Integration
This hook is perfect for globally controlling animation variants in libraries like Framer Motion. We can conditionally set the transition type to none or a simple opacity fade when motion is restricted.
import { motion } from 'motion';
function Card({ children }) {
const shouldReduceMotion = usePrefersReducedMotion();
const animationProps = shouldReduceMotion
? { initial: { opacity: 0 }, animate: { opacity: 1 } }
: { initial: { x: -100 }, animate: { x: 0 }, transition: { type: 'spring' } };
return <motion.div {...animationProps}>{children}</motion.div>;
}Video Auto-play Control
Respecting motion preferences isn't just about CSS transitions; it's also about content. We can use this hook to disable background video auto-play for sensitive users.
function BackgroundVideo({ src }) {
const prefersReducedMotion = usePrefersReducedMotion();
return (
<video
src={src}
autoPlay={!prefersReducedMotion}
muted
loop
controls={prefersReducedMotion}
/>
);
}Why use this?
Tear-Free Synchronization
Traditional media query hooks often use useEffect and useState. This can lead to "tearing"—where different components see different values for the same media query during a single render pass. By using useSyncExternalStore, we ensure that every component in the tree stays perfectly synchronized with the browser's state.
Hydration Consistency
One of the biggest hurdles in SSR is the "hydration mismatch" (e.g., the server thinks the user wants motion, but the client browser says otherwise). This hook uses getServerSnapshot to force a consistent value during the initial hydration, preventing the dreaded "text content does not match" error.
External Store Protocol
Instead of managing local state, we treat the browser's window.matchMedia as an external database. The subscribe function attaches a single event listener that notifies React only when the OS setting actually changes.
Safety First
The serverValue parameter allows you to choose your strategy. While we default to false, some high-accessibility projects might set this to true (Safety First) to ensure no motion happens until the client confirms it is safe.
Error Handling
Testing Preference
You can test this hook in Chrome DevTools by opening the Rendering tab and using the "Emulate CSS media feature prefers-reduced-motion" dropdown.
- Do provide a consistent
serverValueacross your application to avoid flickering during page loads. - Don't use this hook to hide critical content. It should only influence how content appears, not if it appears.
- Warning: Ensure this hook is only used in Client Components (
'use client'), aswindow.matchMediais not available on the server.
Last updated on