DataFree

Animated Counter

A large gradient number that counts from 0 up to its value when it reaches the screen, only once, formatted for the page language.

JSNumbers

Updated

8,547 Active users
Hover or click the scene to interact

This effect is free — the Data category contains 28 effects total, including 1 free. Effect.Labs has 840 vanilla effects. Explore the category →

Usage examples

Our work 2026 report Donate

Annual report 2026

A year alongside 640 families

Three distribution points, a refrigerated van and volunteers every week: here is what your donations made possible.

Scroll down to see the year in numbers

42regular volunteers

3distribution points

11 tof fresh produce

8 547 meals served this year

Thank you to the 1,900 donors who made this year possible.

① Meals served in the year — band in a food bank's report

WhenThe annual report opens with a title and an introduction; further down, a green band announces the meals served. Scrolling the page brings the number on screen and it climbs to 8,547.
WhyA report is read from top to bottom: a number that counts just as the reader reaches it closes the introduction and gives weight to the main result, without catching the eye too early.
SettingsRoot as a 180 px band: --ctup-bg:#123b2a, --ctup-from:#fde68a, --ctup-to:#f59e0b, --ctup-size:4.2rem, --ctup-height:180px, data-target="8547". The page scrolls inside the scene: that scroll is what starts the count.

Local petition

Make the Tilleuls crossroads safe

A lit pedestrian crossing in front of the bus stop, asked for by the residents.

② Signatures of a neighbourhood petition — the number inside the thank-you message

WhenThe resident types a first name and street, clicks "I sign"; the form gives way to a thank-you note where the number of signatures rises to 1,204, with the goal written below.
WhyAfter a civic gesture, seeing the total move confirms the signature counted and shows the signer is not alone; the number stays small and does not steal the message's place.
SettingsTransparent background (--ctup-bg:transparent), --ctup-height:0px, --ctup-size:2.6rem, gradient #4f46e5→#9333ea, grey label --ctup-muted:#64748b, left-aligned by the scene CSS. The panel is hidden (hidden) until the click: the counter starts when it appears.
Pasquelin Thursday 12 March
MTWTFSS

Since Monday: 23.1 km walked

Today's walks

8 h

Along the canal2.1 km · 27 min

12 h

Lunch break1.8 km · 22 min

18 h

Park loop2.5 km · 31 min

③ Distance walked today — tab of a walking app

WhenThe app opens on the week view; a tap on the "Today" tab shows the day's distance, which counts up to 6.4 km with its decimal, above the 8 km goal.
WhyIn an activity tracker, today's number rewards the effort: watching it climb when the tab opens makes the statistic feel alive rather than a figure set in place.
Settingsdata-target="6.4" (one decimal, shown "6.4" in English), --ctup-bg:transparent, --ctup-size:4.4rem, gradient #5eead4→#38bdf8. The day panel starts hidden; showing it is enough to start the count.

How it works

The HTML already holds the final value: <span class="ctup-value" data-target="8547">8,547</span>. The script looks for every .ctup-value[data-target] inside a .fx-0223-root, resets the display to 0, then waits until the number is half visible (IntersectionObserver, threshold 0.5). It then counts up to data-target over 2 seconds, slowing down at the end (cubic ease-out), and never starts again.

The number of decimals comes from data-target itself: 6.4 shows one decimal, 4.85 shows two, 8547 none. The format follows the closest lang attribute (usually the one on <html>) through Intl.NumberFormat: "8,547" and "6.4" on an English page, "8 547" and "6,4" in French. Digits share the same width (tabular-nums), so the block does not wobble while counting.

Every number is handled on its own: several counters in one root, several roots in the page, each one starts when it becomes visible. The script declares nothing on window. A number revealed later (tab, collapsed panel, hidden removed) counts at the moment it appears.

Accessibility

  • Screen readers: the moving number carries aria-hidden="true" and the script adds a visually hidden text right after it (.ctup-sr) with the final value. Users hear "8,547 Active users", never an intermediate value or a "0".
  • Reduced motion: with prefers-reduced-motion: reduce, the final value is set at once, without counting.
  • Contrast: the indigo→pink gradient stays readable on the rgb(26, 26, 36) background; on a light background, change --ctup-from, --ctup-to and --ctup-muted to keep enough contrast.
  • Nothing to operate: the counter has no click and no focus, it reads like plain text.

Browser compatibility

Dependency-free JavaScript (about 2 KB) and CSS variables. Measured on 08/11/2026 in Chromium 151, Firefox 153 and WebKit 26.5 (Playwright): counting on appearance, a counter further down waiting for the scroll, two copies, 390 px, reduced motion, French and English formats, no error. Versions taken from @mdn/browser-compat-data: IntersectionObserver sets the minimum.

Chrome 51+✓ Full (measured)
Firefox 55+✓ Full (measured)
Safari 12.1+✓ Full (measured in WebKit)
Edge 79+✓ Full (Chromium engine)
iOS Safari 12.2+✓ Not checked on a real device
Android Chrome 51+✓ Full (Chromium engine)

Without IntersectionObserver, the final value is shown directly. Without JavaScript, readers see the text written in the HTML (8,547): keep it identical to data-target.

The code

Copy the three blocks into your page. No dependencies.

HTML
<div class="fx-0223-root">
  <div class="ctup-box">
    <span class="ctup-value" data-target="8547">8,547</span>
    <span class="ctup-label">Active users</span>
  </div>
</div>
CSS
.fx-0223-root,
.fx-0223-root *,
.fx-0223-root *::before,
.fx-0223-root *::after {
  box-sizing: border-box
}

.fx-0223-root {
  --ctup-bg: rgb(26, 26, 36);
  --ctup-from: #6366f1;
  --ctup-to: #d946ef;
  --ctup-muted: rgba(255, 255, 255, .6);
  --ctup-size: 3rem;
  --ctup-height: 250px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: var(--ctup-height);
  overflow: hidden;
  position: relative;
  background: var(--ctup-bg);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.ctup-box {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.ctup-value {
  font-size: var(--ctup-size);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  background: linear-gradient(135deg, var(--ctup-from), var(--ctup-to));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.ctup-label {
  font-size: 0.875rem;
  color: var(--ctup-muted);
  text-transform: uppercase;
  letter-spacing: 2px;
}

/* Final value for screen readers (the moving number is hidden from them) */
.ctup-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
JavaScript (fx-0223)
(function () {
  'use strict';
  // Animated Counter: the number counts from 0 up to data-target when half visible, only once.
  var calm = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : { matches: false };

  // Page language (closest lang attribute): 8 547 in French, 8,547 in English
  function pageLang(el) {
    var host = el.closest('[lang]');
    return (host && host.lang) || navigator.language || 'fr';
  }

  function init(el) {
    if (el.hasAttribute('data-ctup-ready')) return;
    el.setAttribute('data-ctup-ready', '');
    var raw = (el.getAttribute('data-target') || '0').trim();
    var target = parseFloat(raw) || 0;
    var decimals = raw.indexOf('.') > -1 ? raw.split('.')[1].length : 0;
    var duration = parseFloat(el.getAttribute('data-duration')) || 2000;
    var fmt = new Intl.NumberFormat(pageLang(el), {
      minimumFractionDigits: decimals,
      maximumFractionDigits: decimals
    });

    // Screen readers read the final value, never the counting steps
    var sr = document.createElement('span');
    sr.className = 'ctup-sr';
    sr.textContent = fmt.format(target);
    el.parentNode.insertBefore(sr, el.nextSibling);
    el.setAttribute('aria-hidden', 'true');

    if (calm.matches || !('IntersectionObserver' in window)) {
      el.textContent = fmt.format(target);
      return;
    }
    el.textContent = fmt.format(0);

    function count() {
      if (calm.matches) {
        el.textContent = fmt.format(target);
        return;
      }
      var start = performance.now();
      requestAnimationFrame(function step(t) {
        var p = Math.min((t - start) / duration, 1);
        var v = target * (1 - Math.pow(1 - p, 3)); // cubic ease-out
        el.textContent = fmt.format(decimals ? v : Math.floor(v));
        if (p < 1) requestAnimationFrame(step);
        else el.textContent = fmt.format(target);
      });
    }

    var obs = new IntersectionObserver(function (entries) {
      if (!entries[0].isIntersecting) return;
      obs.disconnect();
      count();
    }, { threshold: 0.5 });
    obs.observe(el);
  }

  document.querySelectorAll('.fx-0223-root .ctup-value[data-target]').forEach(init);
})();

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Value (HTML — data-target on .ctup-value) 8547 Number reached at the end of the count. Write decimals with a dot (12.5): the number of digits after the dot sets the decimals shown.
Duration (HTML — data-duration on .ctup-value) 2000 (milliseconds) Length of the count, for instance data-duration="1200" for a secondary figure.
Label (HTML — text of .ctup-label) Active users Text below the number, set in spaced capitals by the CSS. Screen readers read it after the value.
Number colours (CSS — --ctup-from and --ctup-to) #6366f1 → #d946ef The two shades of the gradient painted on the number.
Background and label (CSS — --ctup-bg and --ctup-muted) rgb(26, 26, 36) · white at 60% On a light page: --ctup-bg:transparent; --ctup-muted:#475569.
Number size (CSS — --ctup-size) 3rem Large band: 4 to 5rem; inside a card: 2 to 2.5rem.
Block height (CSS — --ctup-height) 250px Minimum height of the root; 0px so it only takes the room of the number and label.
Format language (HTML — lang attribute) the page's, otherwise the browser language To force a format, set lang="fr" (or another) on the root or a parent.

FAQ

Can the counter start again every time it comes on screen?

No, it counts once: the observer disconnects after the first pass. This is on purpose for a page read in both directions. For a counter whose value changes, see Stat Cards: changing its data-target counts again.

How do I show "12.5%" or "$3,200"?

Set data-target="12.5" and write the unit in the label ("% of happy customers"), or next to the span in an element of your own. The script only touches the text of .ctup-value: everything around it stays yours.

My number sits in a hidden tab: does it start too early?

No. An element with display: none is never "visible" to IntersectionObserver: the count waits until the tab is open and the number is half on screen.