FadeIn and AnimatedNumber are scroll-gated, so they break always-on dashboards #1

Closed
opened 2026-07-30 12:43:22 -05:00 by jason · 0 comments
Owner

Problem

Both motion primitives only activate once scrolled into view:

  • src/motion/index.tsxFadeIn uses whileInView with viewport={{ once: true, margin: "-10% 0px" }}
  • src/animated/animated-number.tsxAnimatedNumber uses useInView(ref, { once: true, margin: "-20% 0px" })

That is correct for marketing pages. It is wrong for dashboards nobody scrolls — wall-mounted / always-on displays, or just a short browser window.

Two concrete failures:

  1. AnimatedNumber sticks at 0. The count-up only runs inside if (inView) mv.set(value). On a short viewport the negative margin shrinks the detection band enough that a stat tile never qualifies, so it renders a permanent 0 while the real value sits in the sr-only span. A stat tile silently showing the wrong number is worse than no animation.
  2. FadeIn leaves content invisible. Primary content below the fold stays at opacity: 0 indefinitely.

Reproduce

Render <StatCard label="On time" animate={4} /> (or any FadeIn) so it sits below roughly 80% of viewport height, at a viewport around 800x460, and don't scroll. The tile shows 0.

Hit while adopting the kit in jason/unifi-access-dashboard — an attendance board that runs full-screen on a wall display.

Suggested fix

Give both an opt-out of the scroll gate, defaulting to today's behaviour so existing callers are unaffected:

<FadeIn immediate />                     // animate on mount
<AnimatedNumber value={4} immediate />   // count up on mount

Implementation is small — const active = immediate || inView; in AnimatedNumber, and swapping whileInView for animate in FadeIn when immediate is set.

Alternatively AnimatedNumber could always land on the correct value and treat the animation as the optional part — if not in view, setDisplay(format(value)) rather than leaving 0. That fixes the correctness bug even without a new prop, and is arguably right regardless.

Workaround in the meantime

Keep them above the fold, or use StatCard's static value prop instead of animate.

## Problem Both motion primitives only activate once scrolled into view: - `src/motion/index.tsx` — `FadeIn` uses `whileInView` with `viewport={{ once: true, margin: "-10% 0px" }}` - `src/animated/animated-number.tsx` — `AnimatedNumber` uses `useInView(ref, { once: true, margin: "-20% 0px" })` That is correct for marketing pages. It is wrong for dashboards nobody scrolls — wall-mounted / always-on displays, or just a short browser window. Two concrete failures: 1. **`AnimatedNumber` sticks at `0`.** The count-up only runs inside `if (inView) mv.set(value)`. On a short viewport the negative margin shrinks the detection band enough that a stat tile never qualifies, so it renders a permanent **0** while the real value sits in the `sr-only` span. A stat tile silently showing the wrong number is worse than no animation. 2. **`FadeIn` leaves content invisible.** Primary content below the fold stays at `opacity: 0` indefinitely. ## Reproduce Render `<StatCard label="On time" animate={4} />` (or any `FadeIn`) so it sits below roughly 80% of viewport height, at a viewport around 800x460, and don't scroll. The tile shows `0`. Hit while adopting the kit in `jason/unifi-access-dashboard` — an attendance board that runs full-screen on a wall display. ## Suggested fix Give both an opt-out of the scroll gate, defaulting to today's behaviour so existing callers are unaffected: ```tsx <FadeIn immediate /> // animate on mount <AnimatedNumber value={4} immediate /> // count up on mount ``` Implementation is small — `const active = immediate || inView;` in `AnimatedNumber`, and swapping `whileInView` for `animate` in `FadeIn` when `immediate` is set. Alternatively `AnimatedNumber` could always land on the correct value and treat the animation as the optional part — if not in view, `setDisplay(format(value))` rather than leaving `0`. That fixes the correctness bug even without a new prop, and is arguably right regardless. ## Workaround in the meantime Keep them above the fold, or use `StatCard`'s static `value` prop instead of `animate`.
jason changed title from scope probe to FadeIn and AnimatedNumber are scroll-gated, so they break always-on dashboards 2026-07-30 12:44:02 -05:00
GROK closed this issue 2026-08-20 17:18:50 -05:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jason/ui-kit#1