useReducedMotion hook
interlace-ui/use-reduced-motionFreeUtility
@interlace/ui — the `useReducedMotion` hook every interactive primitive uses to gate animations on the user's OS preference.
Install it
npx shadcn@latest add https://ds.interlace.tools/r/use-reduced-motion.jsonSource
1'use client';23import { useCallback, useSyncExternalStore } from 'react';45// @interlace/use-reduced-motion v1.1.0 — Interlace design system.6// Docs, props and live preview: https://ds.interlace.tools/c/use-reduced-motion7// What changed since: https://ds.interlace.tools/c/use-reduced-motion#history8// Generated banner — keep it, the upgrade diff reads this version.910const QUERY = '(prefers-reduced-motion: reduce)';1112/**13 * Returns `true` when the user has `prefers-reduced-motion: reduce` set.14 *15 * Use to gate motion-heavy components: animation should be disabled or16 * dramatically reduced when this returns `true`.17 *18 * ## Why `useSyncExternalStore` and not `useState` + `useEffect`19 *20 * The canonical hook — the one MUI, Vercel, Linear and Stripe all ship — is21 * `useState(false)` plus an effect that calls `matchMedia` on mount. It is22 * SSR-safe, and it is **one frame late**: the first render always returns23 * `false`, so a component that gates on it paints its animated first frame and24 * only then snaps to the still state.25 *26 * For most gates that is invisible. For the ones this package ships it is not —27 * the frame that gets painted is `AnimatedList`'s `scale: 0`, `FlipWords`' 8px28 * blur, `Spotlight`'s `opacity: 0`. A user who set the preference precisely29 * because motion makes them ill gets one frame of exactly the motion they30 * turned off, on every mount. WCAG 2.3.3 is not satisfied by "briefly".31 *32 * `useSyncExternalStore` reads the store DURING the first render, so on a33 * client-rendered mount — which is where every decorative component in this34 * package actually lives — the first painted frame is already correct.35 *36 * **The honest statement is that this closes the gap on CSR and cannot close it37 * on hydration.** The server genuinely cannot know the preference, so38 * `getServerSnapshot` must return `false` and the hydration frame is39 * unavoidable for any JavaScript hook. The only thing that closes THAT is CSS,40 * which is why `preflight.css` clamps `animation-duration` and41 * `transition-duration` under `reduce` for `*`: that reset is live before the42 * first paint and needs no JavaScript at all.43 *44 * So this hook is for the motion CSS cannot reach — `motion/react`,45 * `requestAnimationFrame`, timer-driven steps. The registry publishes that46 * split per component as `a11y.motion.driver`.47 *48 * @example49 * ```tsx50 * const reduceMotion = useReducedMotion();51// … truncated
More from interlace-ui
All 140 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| cn — class-name merge utilitycn | interlace-ui | Utilities | Free | no deps | |
| No-flash theme bootstrap scripttheme-script | interlace-ui | Utilities | Free | no deps | |
| Theme token manifesttheme-tokens | interlace-ui | Utilities | Free | no deps | |
| useTheme hookuse-theme | interlace-ui | Utilities | Free | no deps |
