Count Up

A key number climbs from zero to its value as it scrolls into view, then eases to a stop so the final figure lands with weight.

  • GSAP
  • Text
  • Entrance
  • Added
12,480Teams onboard

AI prompt

Create a count-up statistic with GSAP. Tween a plain object { n: 0 } to the target value 12480 over 1.6s with ease "power3.out", and on each update write Math.round(n) formatted with toLocaleString() into the number element. Start the tween when an IntersectionObserver sees the stat at 50% visibility, and add a Replay button that restarts it. Use tabular-nums on the number so the width does not jitter while it counts. Put the final value in aria-label so screen readers hear one number, not every frame. Under prefers-reduced-motion, show the final value immediately. Revert the GSAP context on unmount. The caption reads "Teams onboard".

Source

Demo.tsx
'use client'

import { useEffect, useRef } from 'react'
import gsap from 'gsap'
import s from './demo.module.css'

export default function CountUp({ labels }: { labels: Record<string, string> }) {
  const num = useRef<HTMLSpanElement>(null)
  const tween = useRef<gsap.core.Tween | null>(null)
  const target = Number(labels.value)

  useEffect(() => {
    const el = num.current
    if (!el) return
    const show = (n: number) => (el.textContent = Math.round(n).toLocaleString())
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return void show(target)
    const state = { n: 0 }
    show(0)
    const ctx = gsap.context(() => {
      tween.current = gsap.to(state, {
        n: target,
        duration: 1.6,
        ease: 'power3.out',
        paused: true,
        onUpdate: () => show(state.n),
      })
    })
    const io = new IntersectionObserver(
      ([e]) => {
        if (!e.isIntersecting) return
        tween.current?.play()
        io.disconnect()
      },
      { threshold: 0.5 },
    )
    io.observe(el)
    return () => {
      io.disconnect()
      ctx.revert()
    }
  }, [target])

  return (
    <div className={s.wrap}>
      <span ref={num} className={s.num} aria-label={target.toLocaleString()}>
        {target.toLocaleString()}
      </span>
      <span className={s.caption}>{labels.caption}</span>
      <button type="button" className={s.replay} onClick={() => tween.current?.restart()}>
        {labels.replay}
      </button>
    </div>
  )
}
demo.module.css
.wrap {
  display: grid;
  justify-items: center;
  gap: 6px;
}

.num {
  font: 700 clamp(2.4rem, 8vw, 3.2rem) / 1 var(--font-display);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  color: var(--stage-ink);
}

.caption {
  margin-bottom: 10px;
  font: 500 var(--text-xs) / 1 var(--font-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--stage-muted);
}

.replay {
  height: 32px;
  padding: 0 12px;
  border: 1px solid var(--stage-rule);
  border-radius: var(--radius-sm);
  background: var(--stage-surface);
  color: var(--stage-ink);
  font: 500 var(--text-sm) / 1 var(--font-body);
  white-space: nowrap;
  cursor: pointer;
}

.replay:hover { border-color: var(--stage-accent); }
.replay:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.