Stats sit at zero until the section enters the viewport, then tick up to their target over ~1.8s. The signature "by-the-numbers" moment on every modern marketing page.
When a stats section scrolls into view, the numbers visibly count from zero up to their final value over about 1.5 to 2 seconds with an ease-out curve. It's a small, slightly theatrical detail — but it consistently makes the reader pause on the impressive numbers instead of skimming past them. Used by GitHub, Vercel, every annual-report page, every "by the numbers" section.
Each number element has a data-target attribute with the final value. An IntersectionObserver watches them — once a stat is at least 30% visible, an animation runs that lerps the displayed value from 0 to the target over ~1800ms, using requestAnimationFrame and an ease-out curve (like 1 - (1-t)^3). The displayed value is formatted with thousand separators (Intl.NumberFormat) and the suffix/prefix (e.g. "k", "%", "$") is rendered as a smaller span next to it.
Scroll inside. The stats start at zero — they count up the moment they enter the viewport.
Build a "by the numbers" stats section with animated counters. Each stat is a 4-column grid item with a giant number (font-size clamp(48px,7vw,84px), font-weight 300, font-variant-numeric:tabular-nums so digits don't jitter) above a small uppercase monospace label. Add an optional prefix ($) or suffix (% / k / m) as a smaller span next to the number. Each number element has data-counter and data-target="X" attributes. Use IntersectionObserver (threshold 0.3) to detect when each stat enters the viewport — on first intersection, run a requestAnimationFrame loop that lerps the displayed value from 0 to data-target over 1800ms with an ease-out-cubic curve (1 - Math.pow(1 - t, 3)). Format the displayed value with Intl.NumberFormat for thousand separators. Unobserve after the animation completes so it doesn't re-trigger on scroll-back. Stat values: $84,000,000 funded · 12,408 active makers · 99.96% uptime · 11m support reply.