useReducedMotion

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

accessibilityreduced-motion

Why this hook matters

useReducedMotion lets a component respect the user's reduced-motion preference without changing what the interface means. The custom hook is the default copyable code below; 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 https://ui.ericts.com/r/use-reduced-motion.json

After installation

The installed hook stays intentionally small: it reads matchMedia after mount, updates immediately, and cleans up the change listener. The preview controls above are only local demo states; production components can call useReducedMotion() with no arguments and branch their animation feedback from that value.

Motion API as an alternative

If a project already uses Motion for most transitions, Motion's own useReducedMotion hook can replace the custom hook. Keep the UI state the same, then change the animated values that create large visual travel.

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
    }} />
  )
}

You can also set the behavior once for a subtree with MotionConfig. This is useful when Motion already owns the animation tree and should follow the user's device preference by default.

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

// ...

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