← Numbers

Count-up

A number counts up to its value when it scrolls into view, easing off at the end, and keeps your formatting: currency signs, commas, decimals, % and +.

12,480+teams
98.6%uptime
$4.2Mraised

How to use it

  1. Paste the snippet just before </head>.
  2. Put the final number in an element with data-count-up, written exactly as it should end up, e.g. $1,250.50.

Options

Add any of these attributes to the same element.

data-fromStarting value. Default 0.
data-durationTime to count. Default 2000 ms.
data-loopCount again every few seconds.
data-holdPause before recounting, when looping. Default 2200 ms.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Count-up: a number counts up to its value when it scrolls into view, keeping
    // your formatting: prefixes, suffixes, thousands separators and decimals.
    // Usage: <span data-count-up>$1,250.50</span>   <span data-count-up>98%</span>
    // Optional: data-from (start value, default 0), data-duration (ms), data-loop
    // (count again every few seconds), data-hold (ms before recounting).
    var DEFAULTS = { duration: 2000, hold: 2200 };

    function parse(text) {
      var m = text.match(/^(\D*?)(-?[\d,]*\.?\d+)(.*)$/);
      if (!m) return null;
      var num = m[2];
      var decimals = num.indexOf('.') >= 0 ? num.split('.')[1].length : 0;
      return { prefix: m[1], suffix: m[3], value: parseFloat(num.replace(/,/g, '')), decimals: decimals, commas: num.indexOf(',') >= 0 };
    }

    function format(n, f) {
      var s = Math.abs(n).toFixed(f.decimals);
      if (f.commas) {
        var parts = s.split('.');
        parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
        s = parts.join('.');
      }
      return f.prefix + (n < 0 ? '-' : '') + s + f.suffix;
    }

    function run(el, f) {
      var d = el.dataset;
      var state = { n: +d.from || 0 };
      el.textContent = format(state.n, f);
      anime.animate(state, {
        n: f.value,
        duration: +d.duration || DEFAULTS.duration,
        ease: 'out(4)',
        onUpdate: function () { el.textContent = format(state.n, f); },
        onComplete: function () {
          el.textContent = format(f.value, f);
          if (d.loop !== undefined && d.loop !== 'false') setTimeout(function () { run(el, f); }, +d.hold || DEFAULTS.hold);
        }
      });
    }

    function init() {
      var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          run(e.target, e.target._cu);
        });
      }, { threshold: 0.5 });
      document.querySelectorAll('[data-count-up]').forEach(function (el) {
        if (el._cu) return;
        var f = parse(el.textContent.trim());
        if (!f) return;
        el._cu = f;
        el.setAttribute('aria-label', el.textContent.trim());
        if (reduced) return;
        // Hold the final width so the layout doesn't jiggle while counting.
        el.style.display = 'inline-block';
        el.style.fontVariantNumeric = 'tabular-nums';
        el.style.minWidth = el.getBoundingClientRect().width + 'px';
        el.textContent = format(+el.dataset.from || 0, f);
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<p><span data-count-up>12,480+</span> teams use it</p>
<p><span data-count-up>98.6%</span> uptime</p>