Hone logo
Hone
Problems

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:

  1. Detect prefers-reduced-motion: The hook must accurately read the prefers-reduced-motion media query from the browser.
  2. Return a boolean: The hook should return true if prefers-reduced-motion is enabled (meaning animations should be reduced or disabled), and false otherwise.
  3. Reactive to changes: The hook should be reactive. If the user changes their prefers-reduced-motion setting while the application is running, the hook's return value should update accordingly.
  4. Server-Side Rendering (SSR) consideration: For applications that use SSR, the hook should gracefully handle the server environment where window might not be available or the media query cannot be directly evaluated. In SSR, it's generally best to default to false (animations enabled) or a user-configurable default, as the client-side will then correctly determine the preference.

Expected Behavior:

  • When prefers-reduced-motion is active in the browser, any component using useReducedMotion should receive true.
  • When prefers-reduced-motion is not active, components should receive false.
  • 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-motion media query can have two values: no-preference (animations are desired) and reduce (animations should be reduced or disabled). Your hook needs to detect the reduce value.
  • 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 useState and useEffect hooks from React.
  • Remember to subscribe to changes in the prefers-reduced-motion query to make your hook reactive. The MediaQueryList object returned by window.matchMedia has an addEventListener method for this purpose.
  • Ensure proper cleanup of event listeners when the component unmounts to prevent memory leaks.
Loading editor...
typescript