35 lines
1.3 KiB
TypeScript
35 lines
1.3 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|||
|
|
|
||
|
|
const REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)';
|
||
|
|
|
||
|
|
const readReducedMotion = (): boolean =>
|
||
|
|
typeof window !== 'undefined' &&
|
||
|
|
typeof window.matchMedia === 'function' &&
|
||
|
|
window.matchMedia(REDUCED_MOTION_QUERY).matches;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Reactively tracks the OS `prefers-reduced-motion: reduce` setting.
|
||
|
|
*
|
||
|
|
* Unlike a one-off `window.matchMedia(...).matches` read, this subscribes to the
|
||
|
|
* media query's `change` event, so toggling the OS setting mid-session updates
|
||
|
|
* the returned value (and any animation gated on it) without a page reload.
|
||
|
|
* SSR/undefined-safe: returns `false` when `window`/`matchMedia` is unavailable.
|
||
|
|
*/
|
||
|
|
export function useReducedMotion(): boolean {
|
||
|
|
const [reduced, setReduced] = useState<boolean>(readReducedMotion);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
||
|
|
return undefined;
|
||
|
|
}
|
||
|
|
const mql = window.matchMedia(REDUCED_MOTION_QUERY);
|
||
|
|
const onChange = (event: MediaQueryListEvent) => setReduced(event.matches);
|
||
|
|
// Re-sync in case the setting changed between the initial render and this effect.
|
||
|
|
setReduced(mql.matches);
|
||
|
|
mql.addEventListener('change', onChange);
|
||
|
|
return () => mql.removeEventListener('change', onChange);
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
return reduced;
|
||
|
|
}
|