React useReducedMotion Hook
Objective: Create a custom React hook, useReducedMotion, that intelligently detects and respects the user's prefers-reduced-motion accessibility setting. This hook will be invaluable for building more inclusive web applications by conditionally disabling or reducing animations for users who prefer less motion.
Problem Description
Your task is to develop a reusable React hook named useReducedMotion that determines whether the user has enabled the prefers-reduced-motion media query. This setting is a crucial accessibility feature that allows users to indicate that they prefer less animation on their devices.
Key Requirements:
- Detect
prefers-reduced-motion: The hook must accurately read theprefers-reduced-motionmedia query from the browser. - Return a boolean: The hook should return
trueifprefers-reduced-motionis enabled (meaning animations should be reduced or disabled), andfalseotherwise. - Reactive to changes: The hook should be reactive. If the user changes their
prefers-reduced-motionsetting while the application is running, the hook's return value should update accordingly. - Server-Side Rendering (SSR) consideration: For applications that use SSR, the hook should gracefully handle the server environment where
windowmight not be available or the media query cannot be directly evaluated. In SSR, it's generally best to default tofalse(animations enabled) or a user-configurable default, as the client-side will then correctly determine the preference.
Expected Behavior:
- When
prefers-reduced-motionis active in the browser, any component usinguseReducedMotionshould receivetrue. - When
prefers-reduced-motionis not active, components should receivefalse. - Changes to the user's OS-level accessibility settings should be reflected in the hook's output.
Examples
Example 1:
import React from 'react';
import useReducedMotion from './useReducedMotion'; // Assuming your hook is in this file
function AnimatedButton() {
const prefersReducedMotion = useReducedMotion();
const animationStyle = {
transition: prefersReducedMotion ? 'none' : 'transform 0.3s ease-in-out',
transform: prefersReducedMotion ? 'none' : 'scale(1.1)',
};
return (
<button style={animationStyle}>
Click Me
</button>
);
}
Input: User has prefers-reduced-motion enabled in their operating system settings.
Output of useReducedMotion(): true
Explanation: The AnimatedButton component receives true, so animationStyle.transition becomes 'none' and animationStyle.transform becomes 'none'. The button will not animate on hover or interaction.
Example 2:
import React from 'react';
import useReducedMotion from './useReducedMotion';
function FadeInText({ children }) {
const prefersReducedMotion = useReducedMotion();
const textStyle = {
opacity: 1,
transition: prefersReducedMotion ? 'none' : 'opacity 1s ease-in-out',
animation: prefersReducedMotion ? 'none' : undefined, // Conditional animation property
};
return (
<div style={textStyle}>
{children}
</div>
);
}
Input: User has prefers-reduced-motion disabled in their operating system settings.
Output of useReducedMotion(): false
Explanation: The FadeInText component receives false, so textStyle.transition is set to 'opacity 1s ease-in-out', and the animation property is allowed to be defined (or remain undefined if not explicitly set to a non-animation value). The text will fade in.
Example 3 (SSR Scenario):
import React from 'react';
import useReducedMotion from './useReducedMotion';
function Modal({ isOpen, children }) {
const prefersReducedMotion = useReducedMotion();
// Simulate modal animation logic
const modalAnimation = prefersReducedMotion ? { display: 'block' } : { animation: 'fadeIn 0.4s ease-out forwards' };
if (!isOpen) return null;
return (
<div style={modalAnimation}>
{children}
</div>
);
}
Input: Application is rendered on the server.
Output of useReducedMotion(): false (or a determined SSR default)
Explanation: During SSR, window might not exist. The hook should default to a state that assumes animations are not reduced. Upon hydration on the client, the hook will correctly evaluate the prefers-reduced-motion setting and update the UI if necessary.
Constraints
- The hook must be implemented in TypeScript.
- The hook should use standard browser APIs (like
window.matchMedia). - The hook should be efficient and not cause unnecessary re-renders.
- The hook should correctly handle the initial render and subsequent updates to the media query.
- The hook should provide a sensible default for server-side rendering environments.
Notes
- The
prefers-reduced-motionmedia query can have two values:no-preference(animations are desired) andreduce(animations should be reduced or disabled). Your hook needs to detect thereducevalue. - Consider how you will handle the initial state during SSR. A common pattern is to return a default value on the server and then re-evaluate on the client during hydration.
- You will likely need to use
useStateanduseEffecthooks from React. - Remember to subscribe to changes in the
prefers-reduced-motionquery to make your hook reactive. TheMediaQueryListobject returned bywindow.matchMediahas anaddEventListenermethod for this purpose. - Ensure proper cleanup of event listeners when the component unmounts to prevent memory leaks.