GitHub

Components / Motion / Counter

Counter beta

Numbers that count up once, when they arrive on screen — machined, not bouncy.

Preview
8,412

Active users

99.98

Uptime %

1,280

Exports made

<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:20px;max-width:520px">
  <div><div class="k-counter" data-count-to="8412" style="font-size:30px">8,412</div><p style="font-size:12px;color:var(--k-ink-3);margin-top:3px">Active users</p></div>
  <div><div class="k-counter" data-count-to="99.98" data-decimals="2" style="font-size:30px">99.98</div><p style="font-size:12px;color:var(--k-ink-3);margin-top:3px">Uptime %</p></div>
  <div><div class="k-counter" data-count-to="1280" data-duration="2000" style="font-size:30px">1,280</div><p style="font-size:12px;color:var(--k-ink-3);margin-top:3px">Exports made</p></div>
</div>
/* KATA Counter */
.k-counter{font-family:var(--k-font);font-variant-numeric:tabular-nums;font-weight:600;letter-spacing:-.02em;color:var(--k-ink)}
// KATA Counter — count up once, when visible.
document.querySelectorAll('[data-count-to]').forEach(function (el) {
  var to = parseFloat(el.dataset.countTo);
  var decimals = parseInt(el.dataset.decimals || '0', 10);
  var dur = parseInt(el.dataset.duration || '1400', 10);
  function fmt(v) {
    return v.toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
  }
  function finish() { el.textContent = fmt(to); }
  if (!('IntersectionObserver' in window) || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
    finish();
    return;
  }
  el.textContent = fmt(0);
  var io = new IntersectionObserver(function (entries) {
    if (!entries[0].isIntersecting) return;
    io.disconnect();
    var t0 = null;
    function tick(t) {
      if (!t0) t0 = t;
      var p = Math.min(1, (t - t0) / dur);
      var eased = 1 - Math.pow(1 - p, 4);
      el.textContent = fmt(to * eased);
      if (p < 1) requestAnimationFrame(tick); else finish();
    }
    requestAnimationFrame(tick);
  }, { threshold: 0.4 });
  io.observe(el);
});

Usage

For stats and hero numbers. Set the target with data-count-to; decimals and duration are attributes too. It runs once, when the element enters the viewport.

Accessibility

The final value is the text content from the start for no-JS and reduced-motion users — the animation only enhances. Screen readers get the settled number.