useReducedMotion

A client-safe React hook for reading prefers-reduced-motion and reacting to preference changes.

accessibilityreduced-motion

Why this hook matters

useReducedMotionlets a component respect the user's reduced-motion preference without changing what the interface means. The local hook gives components the same preference signal without adding a motion-library dependency; the preview controls only simulate Standard and Reduced states so both paths can be compared on the page.

Reduced motion preference
Product list
use-reduced-motion.ts
"use client";

import { useEffect, useRef, useState } from "react";

const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";

export function useReducedMotion(): boolean {
  const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
  const mediaQueryRef = useRef<MediaQueryList | null>(null);

  useEffect(() => {
    const mediaQuery = window.matchMedia(REDUCED_MOTION_QUERY);
    mediaQueryRef.current = mediaQuery;

    const listener = () => {
      setPrefersReducedMotion(mediaQueryRef.current?.matches ?? false);
    };

    listener();

    mediaQuery.addEventListener("change", listener);

    return () => {
      mediaQuery.removeEventListener("change", listener);
    };
  }, []);

  return prefersReducedMotion;
}

Installation

pnpm dlx shadcn@latest add @ericts/use-reduced-motion

Choose the right trade-off

The local hook does one job: it reads prefers-reduced-motion with matchMedia, syncs the current value after mount, and removes its change listener during cleanup. That gives components the same reduced-motion branch point as Motion's useReducedMotion hook, without installing Motion or adding its bundle weight.

Using Motion

If Motion already drives the component's animations, use Motion's useReducedMotion instead. It keeps the decision inside the same API you use for variants, transitions, and animated values, which is usually clearer than mixing in a separate local hook.

sidebar.tsx
import { useReducedMotion, motion } from "motion/react"

export function Sidebar({ isOpen }) {
  const shouldReduceMotion = useReducedMotion();
  const closedX = shouldReduceMotion ? 0 : "-100%";

  return (
    <motion.div animate={{
      opacity: isOpen ? 1 : 0,
      x: isOpen ? 0 : closedX
    }} />
  )
}

For a larger Motion tree, set the preference once with MotionConfig. This is useful when a parent should make every Motion child follow the user's device preference by default.

motion-config.tsx
import { MotionConfig } from "motion/react";

// ...

<MotionConfig reducedMotion="user">{children}</MotionConfig>