Skip to content

Commit b0ca3cf

Browse files
committed
fix(landing): sync reduced-motion mid-entrance in HeroStat
HeroStat's staggered count-up entrance only checked prefers-reduced-motion once on mount, unlike the hero loops in this PR that now listen for the media query's change event. Toggling the preference mid-entrance left the scheduled timers/RAF running instead of snapping to the settled value.
1 parent 9cdd8d1 commit b0ca3cf

1 file changed

Lines changed: 53 additions & 20 deletions

File tree

  • apps/sim/app/(landing)/components/hero/components/hero-stat

apps/sim/app/(landing)/components/hero/components/hero-stat/hero-stat.tsx

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -53,33 +53,66 @@ export function HeroStat() {
5353
const [count, setCount] = useState(COUNT_FROM)
5454

5555
useEffect(() => {
56-
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
56+
const media = window.matchMedia('(prefers-reduced-motion: reduce)')
57+
let raf = 0
58+
let settleTimer: ReturnType<typeof setTimeout>
59+
let revealTimer: ReturnType<typeof setTimeout>
60+
let fillTimer: ReturnType<typeof setTimeout>
61+
let countTimer: ReturnType<typeof setTimeout>
62+
63+
const clearScheduled = () => {
64+
clearTimeout(settleTimer)
65+
clearTimeout(revealTimer)
66+
clearTimeout(fillTimer)
67+
clearTimeout(countTimer)
68+
cancelAnimationFrame(raf)
69+
}
70+
71+
const showSettled = () => {
72+
clearScheduled()
5773
setSettled(true)
5874
setRevealed(true)
5975
setFilled(true)
6076
setCount(COUNT_TO)
61-
return
6277
}
63-
let raf = 0
64-
const settleTimer = setTimeout(() => setSettled(true), SETTLE_AT_MS)
65-
const revealTimer = setTimeout(() => setRevealed(true), REVEAL_AT_MS)
66-
const fillTimer = setTimeout(() => setFilled(true), FILL_AT_MS)
67-
const countTimer = setTimeout(() => {
68-
const start = performance.now()
69-
const tick = (now: number) => {
70-
const t = Math.min((now - start) / COUNT_DUR_MS, 1)
71-
const eased = 1 - (1 - t) ** 3
72-
setCount(Math.round(COUNT_FROM + (COUNT_TO - COUNT_FROM) * eased))
73-
if (t < 1) raf = requestAnimationFrame(tick)
78+
79+
const runEntrance = () => {
80+
setSettled(false)
81+
setRevealed(false)
82+
setFilled(false)
83+
setCount(COUNT_FROM)
84+
settleTimer = setTimeout(() => setSettled(true), SETTLE_AT_MS)
85+
revealTimer = setTimeout(() => setRevealed(true), REVEAL_AT_MS)
86+
fillTimer = setTimeout(() => setFilled(true), FILL_AT_MS)
87+
countTimer = setTimeout(() => {
88+
const start = performance.now()
89+
const tick = (now: number) => {
90+
const t = Math.min((now - start) / COUNT_DUR_MS, 1)
91+
const eased = 1 - (1 - t) ** 3
92+
setCount(Math.round(COUNT_FROM + (COUNT_TO - COUNT_FROM) * eased))
93+
if (t < 1) raf = requestAnimationFrame(tick)
94+
}
95+
raf = requestAnimationFrame(tick)
96+
}, REVEAL_AT_MS)
97+
}
98+
99+
// Re-synced on 'change' - not just on mount - so toggling the preference
100+
// mid-entrance snaps straight to the settled state instead of leaving the
101+
// stagger/count-up to keep running on its own clock.
102+
const syncMotionPreference = () => {
103+
clearScheduled()
104+
if (media.matches) {
105+
showSettled()
106+
return
74107
}
75-
raf = requestAnimationFrame(tick)
76-
}, REVEAL_AT_MS)
108+
runEntrance()
109+
}
110+
111+
syncMotionPreference()
112+
media.addEventListener('change', syncMotionPreference)
77113
return () => {
78-
clearTimeout(settleTimer)
79-
clearTimeout(revealTimer)
80-
clearTimeout(fillTimer)
81-
clearTimeout(countTimer)
82-
cancelAnimationFrame(raf)
114+
media.removeEventListener('change', syncMotionPreference)
115+
clearScheduled()
83116
}
84117
}, [])
85118

0 commit comments

Comments
 (0)