Any new JS-driven animation MUST respect the user’sDocumentation Index
Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
Use this file to discover all available pages before exploring further.
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.
| Need | Use |
|---|---|
| Read the preference in a component | usePrefersReducedMotion() from @/platform/a11y |
| Read the preference outside React | prefersReducedMotion() from @/shared/lib/a11y/scroll |
scrollIntoView / scrollTo / scrollBy | safeScrollIntoView / safeScrollTo / safeScrollBy from @/shared/lib/a11y/scroll |
| Recharts series | Already patched globally by installRechartsReducedMotion() in src/main.tsx — no per-chart change needed |
| Embla carousel | Pass duration: reducedMotion ? 0 : <normal> to useEmblaCarousel |
| Framer Motion / react-spring | Conditionally set transition={{ duration: 0 }} (Framer) or immediate (react-spring) when reduced motion is on |
| Web Animations API | Set duration: 0 (or skip .animate() entirely) when reduced motion is on |
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)