Stats Bars
Count-up metrics on view
Figures that animate the first time they appear
Figures that animate the first time they appear
Build a metrics band with Tailwind CSS on a neutral-900 background. Centre a small uppercase label and a one-line heading, then a four-column grid of metrics separated by hairline vertical rules on md and up. Each metric shows a large white figure with an optional prefix and suffix rendered as smaller muted spans, a medium-weight label under it, and a tiny grey note giving the measurement window. Use figures like 99.98 percent uptime, 2.4 million decisions a day, 41 milliseconds median latency and 14 regions. Then add JavaScript that watches the section with an IntersectionObserver, and the first time it becomes visible animates each figure from zero to its target over about 1.2 seconds using requestAnimationFrame with an ease-out curve, preserving decimal places and thousands separators, and skipping the animation entirely when the visitor prefers reduced motion. <section id="countSection" class="bg-neutral-900 px-6 py-20 text-white">
<div class="mx-auto max-w-6xl">
<div class="text-center">
<p class="text-xs font-medium uppercase tracking-[0.2em] text-neutral-500">By the numbers</p>
<h2 class="mt-3 text-2xl font-semibold tracking-tight sm:text-3xl">What the platform did last quarter</h2>
</div>
<div class="mt-14 grid gap-10 md:grid-cols-4 md:gap-0 md:divide-x md:divide-neutral-800">
<div class="text-center md:px-6">
<p class="text-4xl font-semibold tracking-tight sm:text-5xl">
<span class="count" data-target="99.98" data-decimals="2">0</span><span class="text-2xl text-neutral-500">%</span>
</p>
<p class="mt-2 text-sm font-medium text-neutral-200">Uptime</p>
<p class="mt-1 text-xs text-neutral-500">Rolling 90 days, all regions</p>
</div>
<div class="text-center md:px-6">
<p class="text-4xl font-semibold tracking-tight sm:text-5xl">
<span class="count" data-target="2.4" data-decimals="1">0</span><span class="text-2xl text-neutral-500">M</span>
</p>
<p class="mt-2 text-sm font-medium text-neutral-200">Decisions a day</p>
<p class="mt-1 text-xs text-neutral-500">Peak day: 3.1M on 14 June</p>
</div>
<div class="text-center md:px-6">
<p class="text-4xl font-semibold tracking-tight sm:text-5xl">
<span class="count" data-target="41" data-decimals="0">0</span><span class="text-2xl text-neutral-500">ms</span>
</p>
<p class="mt-2 text-sm font-medium text-neutral-200">Median latency</p>
<p class="mt-1 text-xs text-neutral-500">p99 held under 180 ms</p>
</div>
<div class="text-center md:px-6">
<p class="text-4xl font-semibold tracking-tight sm:text-5xl">
<span class="count" data-target="14" data-decimals="0">0</span>
</p>
<p class="mt-2 text-sm font-medium text-neutral-200">Regions</p>
<p class="mt-1 text-xs text-neutral-500">Three added since January</p>
</div>
</div>
</div>
</section> var section = document.getElementById("countSection");
var counters = Array.prototype.slice.call(section.querySelectorAll(".count"));
var reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
function format(value, decimals) {
return value.toLocaleString("en-US", {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
});
}
function run(el) {
var target = Number(el.dataset.target);
var decimals = Number(el.dataset.decimals);
if (reduced) {
el.textContent = format(target, decimals);
return;
}
var duration = 1200;
var start = performance.now();
function frame(now) {
var t = Math.min((now - start) / duration, 1);
// Ease-out cubic: fast at first, settles gently on the real figure.
var eased = 1 - Math.pow(1 - t, 3);
el.textContent = format(target * eased, decimals);
if (t < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
var observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) return;
counters.forEach(run);
observer.disconnect();
});
},
{ threshold: 0.35 }
);
observer.observe(section);