Skip to content

Count Up

A number that counts up when it first comes into view, pauses off-screen and never starts over.

$npx shadcn@latest add https://framebrick.co/r/count-up.json

From the Tustel template, see it in context.

Years of experience
80+
Projects completed
120+
Happy clients
45+
Awards received
18

Installation

$ npx shadcn@latest add https://framebrick.co/r/count-up.json

Usage

import { CountUp } from "@/components/ui/count-up";

<p className="text-5xl font-semibold">
  <CountUp value={120} from={40} />+
</p>

// a four-digit figure: a fixed time instead of Framer's rate, and separators
<CountUp value={12400} duration={1.6} format={{ useGrouping: true }} />

Props

PropTypeDefaultDescription
valuenumber—Where it ends.
fromnumber0Where it starts.
intervalnumber5Milliseconds per step, as in Framer: +1 every 5 ms, so 0 → 80 takes 0.4 s.
durationnumber—Total time in seconds, instead of the rate: a big number then takes as long as a small one.
formatIntl.NumberFormatOptions—Formatting, e.g. { useGrouping: true } or { notation: "compact" }. Plain digits by default, as in Framer.
localestring"en-US"Locale for `format`. Always explicit, so the server and the browser agree on the digits.
classNamestring—Classes for the number: its size, weight and colour.
  • It counts once its first pixel is in view, carries on from where it was after being off-screen, and never starts over.
  • It counts by time, not with a timer per step: a 5 ms interval is clamped and throttled by the browser, a clock is not. The digits are tabular, so the width does not jitter while they change.
  • The final number is always in the page, in a second element. Screen readers read it (the moving digits are aria-hidden, so 41, 42, 43 are never announced), and it is what shows with reduced motion, in print and without JavaScript. CSS swaps the two, so the markup never depends on reduced motion.
  • Framer's rate suits small numbers: 80 takes 0.4 s, 12 000 would take a minute. Give big ones a `duration`.
  • For tests: with JavaScript on, the element's textContent holds the moving digits and the final number together. Query by role and name, or read the final element (data-count-up-final).

AI prompt

Paste into v0, Lovable, Bolt or Claude to rebuild this component in your own project.

Create a React + Tailwind CSS component called CountUp (no animation library).
- Props: value, from (0), interval (5 — milliseconds per step, as in Framer), duration (seconds; replaces the rate),
  format (Intl.NumberFormat options; plain digits when absent), locale ("en-US", always explicit so the server and the
  browser agree).
- Count by time, not with a timer per step: keep the elapsed milliseconds in a closure, advance it by the frame's delta in
  requestAnimationFrame, and put the progress (0 to 1) in state. The shown number is Math.round(from + (value - from) *
  progress); at progress 1 it is exactly value. The total time is |value - from| * interval, or duration * 1000.
- An IntersectionObserver on the element starts the frame loop when any part of it is on screen and cancels it when it
  leaves. The elapsed time is kept, so it carries on from where it was and never starts over.
- Render two spans inside one span with tabular-nums: the moving digits (aria-hidden, print:hidden, motion-reduce:hidden)
  and the final number (sr-only, print:not-sr-only, motion-reduce:not-sr-only). Add a <noscript><style> that hides the
  moving digits and shows the final number, so a page without JavaScript shows the real value, not the start.
- Do not start the loop when matchMedia("(prefers-reduced-motion: reduce)") matches — read it inside the effect, never
  while rendering: the markup must not depend on it, because the server cannot know it.

Be the first to know

Get new templates and components first

One short email when we release a template or a free component. No spam — unsubscribe anytime.

We use your email only for Framebrick updates. Privacy policy