Honoring prefers-reduced-motion in a portfolio full of motion
Adding a hook and gating every chaos effect, glitch shake, and ASCII keyframe behind a single user preference — without losing the personality of the site.
This portfolio is loud. Chaos tilt on hover, glitch shake, scanlines, an ASCII identity that does an RGB-split dance, animated cursor trails. Most users love it. Some users get nauseous.
The web has a standard for this — prefers-reduced-motion: reduce — and I wasn't honoring it.
The hook
I added one hook with useSyncExternalStore:
const QUERY = "(prefers-reduced-motion: reduce)";
const subscribe = (cb: () => void) => {
const mql = window.matchMedia(QUERY);
mql.addEventListener("change", cb);
return () => mql.removeEventListener("change", cb);
};
const getSnapshot = () => window.matchMedia(QUERY).matches;
const getServerSnapshot = () => false;
export function usePrefersReducedMotion() {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}useSyncExternalStore instead of useState + useEffect because it gives a correct SSR snapshot (false on the server, real value after hydration) and avoids the flash where motion plays for a frame before being disabled.
The application
Every animation that meaningfully moves now reads this flag:
- Chaos tilt → return early in the pointer-move handler
- Glitch shake → swap keyframes for static
{ x: 0, y: 0, filter: "none" } - ASCII RGB shadow layers → animate to
opacity: 0instead of looping - Hover-driven scroll-linked animations → skip the spring
What I didn't change
The static layout. The colors. The cursor effects. The scanline overlay. None of these move; they're decoration.
Reduced motion isn't "remove everything fun." It's "stop moving things that don't need to move to communicate."