#1005 [Quality-2][High] — Respect reduced-motion settings in data transitions.
Data-driven transitions (loading skeletons, route-progress bars, stale-data
fades, and the dashboard activity fetch) now respect the OS-level
prefers-reduced-motion setting and announce their state changes to screen
readers. A shared reactive hook replaces the several hand-rolled
window.matchMedia("(prefers-reduced-motion: reduce)") copies so behaviour is
consistent app-wide.
usePrefersReducedMotion()returns the live value of(prefers-reduced-motion: reduce)viauseSyncExternalStore, so components re-render when the user flips the OS setting at runtime.- SSR / absent-
matchMediaenvironments fall back tofalse. - Exports
REDUCED_MOTION_QUERYso tests and CSS stay in sync.
Refactored consumers (behaviour kept identical, source-of-truth removed):
src/components/ApiCard.tsxsrc/components/FiltersSidebar.tsxsrc/components/BottomSheet.tsxsrc/components/ApiDetailStickyTOC.tsxsrc/pages/ApiUsage.tsxsrc/pages/ApiDetailPage.tsxsrc/pages/MarketplacePage.tsx
- New
@media (prefers-reduced-motion: reduce)block freezes.skeleton,.skeleton--stellar,.api-card-skeleton(and light-theme variants) to a static--surface-softsurface, and stops.button-spinnerrotation. .route-progress-baropacity fade is disabled under reduced motion.- New
.webhook-deliveries-error/.webhook-deliveries-data/.webhook-deliveries-data--stale/.webhook-deliveries-stale-noteclasses replace the page's hardcodedred/orangeinline colors with the semantic--danger/--warningdesign tokens (theme-aware contrast). The stale opacity transition isnoneunder reduced motion.
- When reduced motion is active the simulated activity fetch skips
LOADING_DELAY_MS(delay0) so the skeleton never flashes. - The activity region is marked
aria-busywhile loading. - A polite
LiveRegionannouncesLoading recent activity.→Recent activity loaded.(orNo recent activity yet.) via the pure, unit-tested helperdescribeActivityAnnouncement.
- Loading is announced with
role="status". - Errors are announced with
role="alert". - Stale/refreshing data keeps the old rows visible but sets
aria-busyand dims them via a token-based class instead of an inline animated style.
- The 240 ms delayed exit collapses to
0ms under reduced motion.
Users with prefers-reduced-motion: reduce see static skeletons, a static
progress bar, instant stale-data updates, and no delayed activity skeleton.
Screen-reader users hear loading/success/error/empty transitions for the
dashboard activity and webhook delivery table.
Users without the preference see no change.
- None. No props or interfaces changed.
- Dynamic status/errors/success/loading changes are semantically announced
(
role="status",role="alert",aria-live="polite"). - Color meaning (error, warning) is carried by theme-aware tokens plus text, preserving contrast in both themes.
- Motion is gated behind
prefers-reduced-motion, per WCAG 2.3.3. aria-busyconveys in-progress data transitions without stealing focus.
Run with:
npm test -- src/hooks/__tests__/usePrefersReducedMotion.test.tsx \
src/components/Dashboard.test.tsx src/components/RouteProgressBar.test.tsx \
src/pages/WebhookDeliveries.test.tsxNote: on Windows, run vitest with --pool=forks --poolOptions.forks.singleFork
if multiple files hang with the default thread pool; keep single-file runs for
precise isolation.
- Follows the existing patterns in
docs/api-usage-reduced-motion-fallback.md(Issue #721) anddocs/api-tag-filter-reduced-motion.md(Issue #701).