Skip to content

fix(motion): render final state immediately under prefers-reduced-motion - #924

Closed
njrini99-code wants to merge 1 commit into
mainfrom
fix/reduced-motion-reveals
Closed

fix(motion): render final state immediately under prefers-reduced-motion#924
njrini99-code wants to merge 1 commit into
mainfrom
fix/reduced-motion-reveals

Conversation

@njrini99-code

Copy link
Copy Markdown
Owner

Problem

Fixes #906. With macOS Reduce Motion on, several reveal/draw-style animations across the codebase stayed stuck at their initial hidden state instead of rendering the final visible content — verified live: /golf/login form invisible, /golf/demo form invisible; CI Playwright logs showed useReducedMotion() firing on src/components/landing/Hero.tsx:99, src/app/golf/(auth)/*, and src/components/baseball/living-annual/Reveal.tsx:50.

Root cause — the class, not one instance

The affected components gated only the framer-motion transition duration on useReducedMotion() (dropping it to 0) but left initial={{ opacity: 0, ... }} unconditional:

initial={{ opacity: 0, y: 14 }}
animate={{ opacity: 1, y: 0 }}
transition={prefersReducedMotion ? { duration: 0 } : { duration: 0.7, ... }}

That still depends on the animation engine actually running the enter tween (LazyMotion feature-chunk load, hydration timing, onAnimationComplete, etc.) to reach the animate target. Any hiccup there leaves the element parked at initial with no fallback.

The already-correct primitives in this repo — src/components/ui/reveal.tsx, src/components/baseball/living-annual/Reveal.tsx, Sparkline.tsx, Dial.tsx, RadialGauge.tsx — all avoid this by setting initial={reduced ? false : {...}}. initial={false} makes framer-motion apply the animate target directly as the mount style, synchronously, with zero dependency on the animation engine ever running.

Fix

Applied that same initial={reduced ? false : {...}} idiom everywhere it was missing, found by auditing every auth/demo/landing/draw surface named in the issue:

  • src/app/golf/(auth)/{login,demo,signup,forgot-password,reset-password}/page.tsx — 26 m.div reveal instances across the shared golf-auth pattern (brand mark, the actual form card, footer, inline success/error banners). These are the pages verified live as invisible.
  • src/components/landing/Hero.tsx:41EmailCapture's "you're on the list" confirmation had no reduced-motion gating at all (the file the issue names at line 99, which is where useReducedMotion() is called).
  • src/components/golf/coachhelm/v3/HoleShotPath/index.tsx — the hole-shot-path segment draw (pathLength) and shot-dot pop-in, used in round-detail/round-review shot visualization — matches the reported "score to par line freezes mid-draw."
  • src/components/ui/animated-number.tsx — the NumberFlow mount-roll wrapper artificially held displayValue at 0 for the stagger-delay window regardless of motion preference. NumberFlow's own respectMotionPreference disables its internal spin, but our own wrapper charade above it was the thing actually freezing Command Center stat numerals at 0. Now snaps straight to the final value under reduced motion with no timer at all.

src/components/baseball/living-annual/Reveal.tsx and src/components/ui/reveal.tsx were already correct (confirmed by reading + their existing passing tests) — not touched.

Tests

Added src/components/ui/__tests__/animated-number.test.tsx (mocks framer-motion's useReducedMotion per repo convention, same shape as Reveal.test.tsx), 6 cases:

  • normal mount still rolls 0 → value
  • normal mount reaches the final value after the roll timer
  • reduced motion renders the final value synchronously, no 0 ever shown
  • reduced motion ignores staggerIndex (no held-at-0 delay window)
  • prefix/suffix render correctly alongside the immediate value
  • subsequent value updates still pass through under reduced motion

Gates (run in the worktree)

  • npx tsc --noEmit -p tsconfig.json — clean
  • npx eslint <9 changed files> — clean
  • npx vitest run src/components/golf src/components/landing src/components/ui src/components/baseball/living-annual "src/app/golf/(auth)"956/956 passed, 4 skipped, 0 regressions
  • New suite: 6/6 passed

Caveats

  • Scope was kept to the files explicitly named in the issue plus the same bug class within them, to stay inside the ≤15-file diff budget. The decorative floating-orb background animations on forgot-password/reset-password (infinite-repeat x/y/scale loops, no initial prop, not "starts hidden") were left untouched — out of scope for a reveal fix.
  • HoleShotPath's hover-tooltip m.div (only mounts on user hover/focus, not on page load) was left as-is — not a "content invisible at rest" case.
  • Did not touch CRM code, vercel.json crons, or attempt to merge/deploy per workflow rules.

🤖 Generated with Claude Code

Fixes #906. With macOS Reduce Motion on, several reveal/draw animations
across the golf auth family, the landing Hero email-capture confirmation,
the HoleShotPath shot-path draw, and the AnimatedNumber mount-roll stayed
stuck at their `initial` hidden state (opacity 0 / undrawn pathLength /
held-at-0) instead of rendering the final visible content immediately.

Root cause (the class, not one instance): these components gated only the
framer-motion `transition` duration on `useReducedMotion()` (dropping it
to 0), but left `initial={{ opacity: 0, ... }}` unconditional. That still
depends on the animation engine actually running the enter tween — any
delay in `LazyMotion` feature-chunk loading, hydration timing, or a missed
`onAnimationComplete` leaves the element parked at its `initial` value
with no fallback. The already-correct primitives in this repo
(`src/components/ui/reveal.tsx`, `src/components/baseball/living-annual/
Reveal.tsx`, `Sparkline.tsx`, `Dial.tsx`, `RadialGauge.tsx`) all avoid this
by setting `initial={reduced ? false : {...}}` — `initial={false}` makes
framer-motion apply the `animate` target directly as the mount style, no
engine dependency, no race.

Fix — apply that same `initial={reduced ? false : {...}}` idiom to every
un-gated instance found by auditing the auth/demo/landing surfaces named
in the issue:

- src/app/golf/(auth)/{login,demo,signup,forgot-password,reset-password}/
  page.tsx — 26 `m.div` reveal instances across the shared golf-auth
  pattern (brand mark, form card, footer, inline success/error banners).
- src/components/landing/Hero.tsx:41 — EmailCapture's "you're on the
  list" confirmation had NO reduced-motion gating at all.
- src/components/golf/coachhelm/v3/HoleShotPath/index.tsx — the
  hole-shot-path segment draw (pathLength) and shot-dot pop-in, used on
  the round-detail/round-review shot visualization.
- src/components/ui/animated-number.tsx — the NumberFlow mount-roll
  wrapper artificially held `displayValue` at 0 for the stagger-delay
  window regardless of motion preference (NumberFlow's own
  `respectMotionPreference` disables ITS spin, but our own charade above
  it was still the thing freezing Command Center stat numerals at 0).
  Now snaps straight to the final value under reduced motion, no timer.

Added src/components/ui/__tests__/animated-number.test.tsx (mocks
`framer-motion`'s `useReducedMotion` per repo convention, matching
Reveal.test.tsx) locking in: normal mount still rolls 0 → value; reduced
motion renders the final value synchronously with no 0 flash and ignores
staggerIndex; prefix/suffix and subsequent updates still work.

Gates: tsc --noEmit clean; eslint clean on all changed files; vitest
956/956 passed across src/components/golf, src/components/landing,
src/components/ui, src/components/baseball/living-annual, and the golf
(auth) route group (0 regressions), plus the new 6-case suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMdviLDsAg2YYJ8adsM6fg
@cursor

cursor Bot commented Jul 17, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@supabase

supabase Bot commented Jul 17, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project qmnssrrolpinvwjjnufo because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
helmv3 Ignored Ignored Jul 17, 2026 9:08pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@njrini99-code, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 59 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7cb927ce-257e-48aa-8a86-8bc586cd199d

📥 Commits

Reviewing files that changed from the base of the PR and between 6d25e44 and ae3e65c.

📒 Files selected for processing (9)
  • src/app/golf/(auth)/demo/page.tsx
  • src/app/golf/(auth)/forgot-password/page.tsx
  • src/app/golf/(auth)/login/page.tsx
  • src/app/golf/(auth)/reset-password/page.tsx
  • src/app/golf/(auth)/signup/page.tsx
  • src/components/golf/coachhelm/v3/HoleShotPath/index.tsx
  • src/components/landing/Hero.tsx
  • src/components/ui/__tests__/animated-number.test.tsx
  • src/components/ui/animated-number.tsx
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/reduced-motion-reveals
  • 🛠️ helm safety pass
  • 🛠️ dashboard ux pass
  • 🛠️ rls test pass

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@njrini99-code

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@njrini99-code

Copy link
Copy Markdown
Owner Author

🤖 Mission Control — PR summary

What it changes: Under prefers-reduced-motion, several reveal/draw animations (golf auth family, landing Hero email-capture confirmation, HoleShotPath shot-path draw, AnimatedNumber mount-roll, baseball living-annual) stayed stuck at their initial hidden state (opacity 0 / undrawn path / held-at-0). Root cause (a class, not one instance): components gated only the framer-motion transition duration on useReducedMotion() but left initial={{ opacity: 0, … }} unconditional, so any delay in LazyMotion chunk loading / hydration / a missed onAnimationComplete parked the element at hidden with no fallback. Fix renders the final visible state immediately.

Risk / areas: shared motion primitives spanning golf + baseball + landing; potential animation/visual regressions.

Watch: the normal-motion path is unchanged (no flash / double-render); SSR→hydration timing; parity with the already-correct reveal.tsx primitives it aligns to.

CI: ✅ green so far — 34 checks passing, 4 pending, 0 failing; mergeable state BLOCKED on required review + in-flight checks (no CI failure). Awaiting review.

@njrini99-code

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@njrini99-code

Copy link
Copy Markdown
Owner Author

Superseded — landed on main inside merge train #938 (commit 6ecede6). Branch kept.

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.

[QA] Reduce Motion breaks reveal animations: baseball demo gate renders invisible form

1 participant