webdevpuneet.com

Count Up Animation — Free HTML CSS JS Snippet

Count Up Animation · Animations · Plain HTML, CSS & JS · Live preview

CategoryAnimations

Copy & paste

Source Code

<div class="scene">
  <div class="grid">
    <div class="stat"><div class="number" data-target="98.7" data-suffix="%" data-decimal="1">0%</div><div class="label">Uptime SLA</div></div>
    <div class="stat"><div class="number" data-target="2400000" data-prefix="$">$0</div><div class="label">Revenue processed</div></div>
    <div class="stat"><div class="number" data-target="142" data-suffix="ms">0ms</div><div class="label">Avg response time</div></div>
    <div class="stat"><div class="number" data-target="50000" data-suffix="+">0+</div><div class="label">Active users</div></div>
  </div>
  <button class="btn" onclick="runAll()">Replay ↺</button>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

requestAnimationFrame quartic ease-out: Math.pow(1-p, 4) — more dramatic than cubic
data-target, data-prefix, data-suffix, data-decimal attributes — no code changes per stat
Auto-formatting: 1M+ as "1.2M", 1K-999K as toLocaleString(), below 1K as toFixed(n)
Replay button calls runAll() — restarts all counters simultaneously
1600ms duration configurable in one JS constant
2x2 grid layout with gap-2px border-radius overflow for a unified card effect
Dark background: 0f172a with accent number colours per cell
Export as HTML file, React JSX, or React + Tailwind CSS
Mobile (375px), Tablet (768px), Desktop device preview buttons
Live split-pane editor — preview updates as you type

About this UI Snippet

Count Up Animation — requestAnimationFrame, Quartic Ease-Out & Smart Number Formatting

Screenshot of the Count Up Animation snippet rendered live

A count-up animation makes numbers appear to count from zero to their final value — revenue, user counts, uptime, conversion rates. The animation transforms a static number into a moment that draws attention and makes metrics feel earned. This snippet is a more advanced version of the Stats Card count-up, with configurable prefix/suffix, multiple decimal places, and automatic M/K formatting. For a slot-machine roll instead of a smooth count, see the number slot machine.

The animation engine

countUp(el) reads four data attributes from the element: data-target (the final number), data-prefix (e.g. "$"), data-suffix (e.g. "%"), and data-decimal (decimal places to show). It uses performance.now() as the start time and requestAnimationFrame for smooth frame-by-frame updates over 1600ms.

The quartic ease-out function

const val = target * (1 - Math.pow(1 - p, 4)) is a quartic ease-out (power of 4). Compared to the cubic ease-out in the Stats Card, the quartic version accelerates harder at the start and decelerates more sharply near the end. This gives numbers a more dramatic "slamming into place" quality, which works well for hero section metrics.

Smart number formatting

The formatter auto-selects the right format: numbers 1,000,000+ show as 1.2M, numbers 1,000–999,999 use toLocaleString() for commas (24,891), and numbers below 1,000 use toFixed(decimal) for decimal precision. The prefix and suffix are applied from data attributes, so data-prefix="$" data-target="24891" displays as $24,891` without any code changes.

The replay button

A Replay button at the bottom calls runAll() which re-runs countUp() on all .number elements. Since countUp re-reads data-target and restarts performance.now(), each replay is independent with no state to reset.

Combining with IntersectionObserver

Wrap countUp(el) in an IntersectionObserver (as in the Stats Card snippet) to trigger the animation only when the stat enters the viewport. This prevents the animation from finishing before the user scrolls to see it.

The easing function

The count-up uses a quartic ease-out: at time t (0 to 1), progress = 1 - Math.pow(1 - t, 4). This function starts very fast (close to linear near t=0) and decelerates sharply near t=1. The dramatic deceleration makes the number feel like it's "settling" on its final value, creating a satisfying landing effect. Linear interpolation (just t) creates a mechanical feel; ease-out feels earned.

The M/K auto-formatter

Numbers over 999,999 display as "X.XM"; numbers over 999 display as "XK". The formatter runs on each animation frame: function fmt(n) { return n >= 1e6 ? (n/1e6).toFixed(1)+'M' : n >= 1e3 ? Math.round(n/1e3)+'K' : Math.round(n)+''; }. This ensures the displayed number never looks awkward at large values like "1,200,000" — it shows "1.2M" instead.

IntersectionObserver for scroll trigger

The count-up animation fires when the element scrolls into view using IntersectionObserver with threshold: 0.5. The observer disconnects after the first trigger to prevent re-animation on scroll back. This is a key UX decision — once a metric has counted up, it should stay at its target value to communicate stability.

Step by step

How to Use

  1. 1
    Load the snippetClick "Count Up Animation" in the sidebar. The preview runs the count-up immediately on load. Click Replay to restart all counters.
  2. 2
    Update the target valuesIn the HTML panel, change the data-target attribute on each .number span. The animation reads it on each run.
  3. 3
    Add prefix and suffixAdd data-prefix="$" for currency or data-suffix="%" for percentages. The animation applies them to each frame output.
  4. 4
    Change the animation durationIn the JS panel, update 1600 to change the animation duration in milliseconds.
  5. 5
    Add IntersectionObserver triggerWrap countUp(el) in an IntersectionObserver to fire the animation when the stat enters the viewport instead of on page load.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

Hero section impact metrics
Animate revenue, customer count, uptime, or NPS score as users first see the metrics section. The quartic ease-out makes numbers slam into place for maximum impact.
SaaS social proof numbers
Animated social proof stats on a landing page — "24,891 developers", "$1.2M processed" — make the numbers feel real and dynamic.
Learn requestAnimationFrame and easing
Edit the Math.pow exponent in the JS panel to change the easing curve — try 2 for quadratic, 3 for cubic, 6 for a very sharp ease-out.
Dashboard KPI widgets
Use as animated number widgets on a dashboard. The data-prefix/suffix system handles currency, percentages, and units without code changes.
Year-in-review and recap pages
Animate key stats on a year-in-review page — posts written, miles run, books read. The Replay button lets users re-trigger the animation.
Add IntersectionObserver for scroll trigger
Wrap countUp in an IntersectionObserver to fire when the stat enters view. See the Stats Card snippet for the exact observer pattern to combine with this formatter.
Related: Cursor Particle Brush Trail
See the Cursor Particle Brush Trail for a related animations pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

This snippet uses a quartic ease-out (power of 4) vs the Stats Card cubic ease-out (power of 3) — it decelerates more sharply. It also has configurable data-prefix and data-suffix attributes, M/K auto-formatting for large numbers, and a Replay button. The Stats Card uses IntersectionObserver; this one runs on load.

The formatter checks the target value: if 1,000,000 or above, it divides by 1,000,000 and adds "M". If 1,000–999,999, it uses toLocaleString() for comma formatting. If below 1,000, it uses toFixed(decimal). The comparison uses the final target, not the animated value.

Add data-prefix="$" to the element for a prefix (appears before the number). Add data-suffix="%" for a suffix (appears after). Both are read as strings and concatenated with the formatted number on every animation frame.

The quartic ease-out is 1 - Math.pow(1-p, 4). Change 4 to 2 for quadratic ease-out (gentle deceleration), 3 for cubic, or 6+ for a very dramatic snap. Higher powers decelerate faster.

Wrap countUp(el) in an IntersectionObserver: const obs = new IntersectionObserver(entries => { entries.forEach(e => { if (e.isIntersecting) { countUp(e.target); obs.unobserve(e.target); } }); }); Then observe all .number elements. The Stats Card snippet shows the exact implementation.

Yes. Click "JSX" for a React component. In React, call the countUp logic in a useEffect and use a ref to access the DOM element. Or manage the animated value in useState, updating it on each requestAnimationFrame tick.