prefers-reduced-motion: reduce setting. CSS animations and transitions are already neutralized globally by the @media (prefers-reduced-motion: reduce) rule in src/index.css, but JavaScript-driven motion bypasses that rule and must opt in explicitly.
When this applies
- Chart/graph libraries with internal
requestAnimationFrameinterpolation (Recharts, D3, Chart.js, etc.) element.scrollIntoView/scrollTo/scrollBycalls withbehavior: 'smooth'- Carousels and sliders (Embla, Swiper, keen-slider)
- Web Animations API (
element.animate(...)) and manualrequestAnimationFrameloops - Spring/physics libraries (Framer Motion, react-spring) — gate animation props on the hook
- Auto-scrolling tickers, marquees, parallax, and confetti effects
How to comply
Use the existing helpers — do not re-implement thematchMedia check inline.
Rules
- Never hardcode a non-zero animation duration without checking the preference first when the motion is JS-driven.
- Never write
behavior: 'smooth'directly — use thesafeScroll*helpers. The only allowed exception is when the call site has already early-returned for reduced-motion users (seeScrollableTabsList.tsxfor the pattern). - The reduced-motion path must be functionally equivalent — instant, but the same end state. Do not skip the action, only its animation.
- Both branches must be reactive: if you read
usePrefersReducedMotion(), the component will re-render when the OS toggle changes — make sure your animation re-initializes (e.g., EmblareInit, Framer key change) so the new duration takes effect without a reload. - Tests for new motion code should cover both branches (
matchMediamock returnsmatches: trueandmatches: false).
Reference
- Helpers:
src/shared/lib/a11y/scroll.ts,src/platform/a11y/usePrefersReducedMotion.ts - Recharts patch:
src/shared/lib/a11y/recharts-motion.ts(installed insrc/main.tsx) - Global CSS rule:
src/index.css(@media (prefers-reduced-motion: reduce)) - WCAG: 2.3.3 Animation from Interactions (AAA)