Definition
When the user enables “reduce motion” in the OS, the app explicitly disables JavaScript-driven page transition animations.
prefers-reduced-motion: reduce is the CSS media query the browser uses to report that setting. CSS transition/animation can shrink duration to nearly zero via tokens, but JS spring and MutationObserver-based libraries do not read those CSS variables. So subscribe with matchMedia, and when reduce is active, do not mount the transition Provider or leave transitions empty.
Why It Matters
Accessibility: forced motion harms users with vestibular sensitivity or attention issues. Quality: visual regression (VR) and axe smoke often force reduced motion; if JS transitions still run, intermediate frame snapshots cause flakes. Performance: on pages with frequent DOM changes such as virtual lists, transition Observers subscribing to mutations during scroll become hot-path tax.
How It Works
- Subscribe to
window.matchMedia('(prefers-reduced-motion: reduce)'). - In React, read SSR-safely with
useSyncExternalStore— server/getServerSnapshotis usuallyfalse(motion allowed), corrected to actual value after client hydration. If the setting only changes animation and not DOM text, there is no hydration mismatch. - When reduce is active: (a) empty the transitions array, or (b) do not render the Provider — (b) also removes Observer cost.
- In page transition config, do not preserve or manipulate scroll (
preserveScroll: false, etc.). Leave scroll restoration to Next and virtual list authority.
Practical Application
- Keep site chrome (header/footer) outside transition boundaries so routes do not re-animate each time.
- Keep
reducedMotion: 'reduce'on Playwright VR/a11y projects to lock regressions. - When adding geometric drill transitions for list↔detail, pass scroll-restore e2e first before swapping.
- When using CSS View Transitions, also add a global rule setting animation-duration to 0 under reduced motion.
Trade-offs
| Choice | Upside | Cost |
|---|---|---|
| Do not mount Provider | Removes Observer and spring cost | More branch code and test paths |
| Empty transitions array only | Simple API | Library may still mount |
| CSS reset only | Easy implementation | Ineffective for JS transitions |
When Not to Use
- Delegating scroll restoration to the transition library while also using virtual lists (authority conflict).
- Turning off motion for reduced-motion users when “alternative information” is needed but state change is not communicated (motion ≠ information delivery).
Common Mistakes
- Setting
--duration: 1msintokens.cssonly while leaving JS library unchanged. - Calling
window.matchMediadirectly during SSR, crashing the server. - Transition layer
key={pathname}competing with list scroll restore.
Related Concepts
- usesyncexternalstore-snapshot-identity — Snapshot rules for external store and media subscription
- virtual-list-scroll-restore-authority — Single authority for scroll
- css-view-timeline-entry-animation — CSS scroll/view timeline motion