Skip to content

fix(metrics): respect reduced motion in numeric counters - #4522

Open
jackwalkerlabs wants to merge 2 commits into
umami-software:devfrom
jackwalkerlabs:codex/respect-reduced-motion-3651
Open

jackwalkerlabs wants to merge 2 commits into
umami-software:devfrom
jackwalkerlabs:codex/respect-reduced-motion-3651

Conversation

@jackwalkerlabs

@jackwalkerlabs jackwalkerlabs commented Sep 8, 2026

Copy link
Copy Markdown

Numeric counters now respect the browser's reduced-motion preference. Metric cards, performance cards and list counts display their final values without counting through intermediate numbers. Normal animation and the existing list animate={false} option are preserved.

Addresses the system-preference part of #3651. This does not add an application settings toggle. The shared media-query hook follows preference changes and uses a stable server snapshot to avoid hydration mismatches.

Validation on 8ecc219f6eb24cb248b7dfe962eed8643fa333df, based on dev 9eef6f2:

  • All 852 unit tests passed, including eight new tests for counters, preference changes, listener cleanup, hydration and missing matchMedia. Three counter regressions fail against the original components.
  • pnpm build passed with a disposable PostgreSQL database. Next's existing configuration skips TypeScript errors during build: standalone tsc --noEmit still reports three existing errors, with output identical to unchanged dev (two Row.flexWrap errors and a deep type instantiation in a 2FA test). Repository lint has the same six baseline errors; all six changed files pass Biome check.
  • Factory operator run FC-20260908-B82814 passed both Linux Chromium scenarios with zero retries and no page errors. DOM observations show final values throughout reduced-motion updates and intermediate values during normal animation. Pinned report, samples and source identity.

Browser checks use actual components in a standalone fixture with synthetic data and localization/mobile adapters, not a complete dashboard workflow. They run through Factory's shipped DockerRunner with a separate RunStore, not its production queue.

Component fixture after a reduced-motion update

Test setup: Node 24.20.0, pnpm 10.30.1, frozen lockfile installation with lifecycle scripts disabled, then Prisma generation and workspace builds. The stock Vitest run rejects the existing Next PostCSS configuration. Unit runs used an operator-only config merging vitest.config.ts with { css: { postcss: { plugins: [] } } }; no assertions or test exclusions changed. That override is not part of this PR.

Reviewer QA: enable reduced motion in the OS or browser, load a report, and change its data range. Counts, percentages and performance values should update directly. Disable reduced motion and confirm the counters animate again.

AI disclosure: heavy Codex assistance. Codex implemented, reviewed and ran the checks under the submitter's authorization. No independent human review is claimed.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@vercel

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown

@jackwalkerlabs is attempting to deploy a commit to the Umami Software Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps

greptile-apps Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a reactive, hydration-safe reduced-motion preference hook and uses it to bypass numeric counter animations in metric, performance, and list components.

  • Subscribes counters to browser preference changes.
  • Renders final values directly when reduced motion is enabled.
  • Preserves normal animation and explicit list animation opt-out behavior.
  • Adds focused tests for hydration, subscription cleanup, preference updates, and affected counters.

Confidence Score: 4/5

The PR appears safe to merge, with a non-blocking compatibility gap for browsers exposing only legacy MediaQueryList listener methods.

The reduced-motion behavior and hydration path are coherently implemented and tested; the remaining concern is that the new subscription can throw in legacy browser environments instead of falling back to addListener.

Files Needing Attention: src/components/hooks/useReducedMotion.ts, src/components/hooks/useReducedMotion.test.tsx

Important Files Changed

Filename Overview
src/components/hooks/useReducedMotion.ts Adds a reactive external-store hook with a stable server snapshot, but lacks a fallback for legacy MediaQueryList listener methods.
src/components/metrics/ListTable.tsx Disables count and percentage interpolation under reduced motion while preserving virtualization and explicit animation controls.
src/components/metrics/MetricCard.tsx Jumps springs and directly renders final metric and percentage values when reduced motion is active.
src/components/metrics/PerformanceCard.tsx Bypasses performance-value interpolation for reduced-motion users.
src/components/hooks/useReducedMotion.test.tsx Covers preference updates, cleanup, hydration, and missing matchMedia, but not legacy-only listener APIs.
src/components/metrics/reducedMotion.test.tsx Verifies static reduced-motion output and preservation of existing animated and opt-out paths.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Preference["prefers-reduced-motion"] --> Hook["useReducedMotion"]
  Hook -->|reduce| Static["Render final numeric values"]
  Hook -->|no preference| Springs["Use existing spring animations"]
  Static --> Metric["MetricCard"]
  Static --> Performance["PerformanceCard"]
  Static --> List["ListTable"]
  Springs --> Metric
  Springs --> Performance
  Springs --> List
Loading

Reviews (1): Last reviewed commit: "fix(metrics): keep motion preferences re..." | Re-trigger Greptile

Comment on lines +7 to +8
media?.addEventListener('change', onChange);
return () => media?.removeEventListener('change', onChange);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Legacy media listeners unsupported

If a browser's MediaQueryList supports only the legacy addListener API, optional chaining on media does not protect this addEventListener call. Mounting a metric component then throws instead of rendering. Feature-detect the listener methods and fall back to addListener and removeListener.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant