All files / hooks/use-reduced-motion/src useReducedMotion.ts

100% Statements 22/22
100% Branches 13/13
100% Functions 7/7
100% Lines 20/20

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83                                            1x     17x                                                 12x   12x 9x 3x   6x     12x 9x 2x     7x 7x 9x     7x   7x 6x 6x     1x 1x     12x    
import { useEffect, useState } from "react";
 
/**
 * Options for the useReducedMotion hook.
 */
export interface UseReducedMotionOptions {
  /**
   * Value returned on the server / when `matchMedia` is unavailable.
   * @default false
   */
  defaultValue?: boolean;
  /**
   * When `true` (default), the real `matchMedia` value is read synchronously on
   * the first client render. Set to `false` to render `defaultValue` on the
   * first client render too and defer the real read to a post-commit effect —
   * this avoids a React hydration mismatch when the server rendered
   * `defaultValue` but the user actually prefers reduced motion.
   * @default true
   */
  initializeWithValue?: boolean;
}
 
const REDUCE_QUERY = "(prefers-reduced-motion: reduce)";
 
function isSupported(): boolean {
  return (
    typeof window !== "undefined" && typeof window.matchMedia === "function"
  );
}
 
/**
 * Tracks the user's `prefers-reduced-motion` accessibility setting, returning
 * `true` when the user has requested reduced motion. Updates live when the
 * system setting changes.
 *
 * Use it to disable or tone down animations for users who are sensitive to
 * motion — a baseline accessibility requirement.
 *
 * @param options - Configuration (`defaultValue`, `initializeWithValue`)
 * @returns `true` if the user prefers reduced motion
 *
 * @example
 * ```tsx
 * const reduced = useReducedMotion();
 * <div style={{ transition: reduced ? "none" : "transform 300ms" }} />;
 * ```
 */
export function useReducedMotion(
  options: UseReducedMotionOptions = {}
): boolean {
  const { defaultValue = false, initializeWithValue = true } = options;
 
  const [reduced, setReduced] = useState<boolean>(() => {
    if (!initializeWithValue || !isSupported()) {
      return defaultValue;
    }
    return window.matchMedia(REDUCE_QUERY).matches === true;
  });
 
  useEffect(() => {
    if (!isSupported()) {
      return;
    }
 
    const mediaQueryList = window.matchMedia(REDUCE_QUERY);
    const handleChange = () => {
      setReduced(mediaQueryList.matches === true);
    };
 
    handleChange();
 
    if (typeof mediaQueryList.addEventListener === "function") {
      mediaQueryList.addEventListener("change", handleChange);
      return () => mediaQueryList.removeEventListener("change", handleChange);
    }
 
    mediaQueryList.addListener(handleChange);
    return () => mediaQueryList.removeListener(handleChange);
  }, []);
 
  return reduced;
}