Countup
A number that rises to itself. The figure in the markup is the final value — prefix, suffix, decimals and all — and the component counts from from up to it once the element reaches the viewport, then restores the served text character for character. Nothing is computed, nothing is invented: the only thing JavaScript adds is the climb.
The fixture below counts the measurements of the package itself. Scroll it into view and the four cards climb once; reload with JavaScript off and the same four numbers are simply there.
<p class="iv-u-m-0 iv-u-mb-6">Four figures measured by <code>npm run sizes</code> and <code>npm test</code> on the
<code>0.6.0-beta.0</code> milestone (commit <code>34e69c1</code>): they are the real numbers of the package, not a
demonstration set. Each one is served complete, so the page reads the same without JavaScript, and counts up once when it
reaches the viewport.</p>
<div class="iv-stat-group">
<div class="iv-stat iv-stat--card">
<span class="iv-stat__label">Core stylesheet</span>
<strong class="iv-stat__value iv-count" data-iv-component="countup">2.60</strong>
<span class="iv-stat__hint">KiB min + gzip, budget 8</span>
</div>
<div class="iv-stat iv-stat--card">
<span class="iv-stat__label">Full stylesheet</span>
<strong class="iv-stat__value iv-count" data-iv-component="countup">25.64</strong>
<span class="iv-stat__hint">KiB min + gzip, budget 40</span>
</div>
<div class="iv-stat iv-stat--card">
<span class="iv-stat__label">Bundled JavaScript</span>
<strong class="iv-stat__value iv-count" data-iv-component="countup">39.04</strong>
<span class="iv-stat__hint">KiB min + gzip, budget 48</span>
</div>
<div class="iv-stat iv-stat--card">
<span class="iv-stat__label">Unit tests</span>
<strong class="iv-stat__value iv-count" data-iv-component="countup" data-iv-duration="1400">641</strong>
<span class="iv-stat__hint">27 files, all passing</span>
</div>
</div>
<p class="iv-u-text-sm iv-u-text-muted iv-u-mt-6">With <code>prefers-reduced-motion: reduce</code>, without an
<code>IntersectionObserver</code> or without JavaScript, nothing moves: the served figure is already the answer.</p>// data-iv-component="countup"
{
duration: 1400,
}Formats, prefixes and locales
The format comes from the served text and from the language of the element. A figure written 12.480,50 € inside lang="es" counts in Spanish grouping and keeps its suffix; € 1,240.50 in English keeps its prefix and its comma. decimals is deduced from what was served, and a non-breaking space keeps a currency sign from falling to the next line.
<p class="iv-u-m-0 iv-u-mb-6">Prefixes, suffixes and separators come from the served text and from the language of the
element: the component never invents a format. The three money figures below are <strong>made up for this
fixture</strong>; the test count is the real one of the <code>0.6.0-beta.0</code> milestone.</p>
<div class="iv-stat-group" style="--iv-stat-group-min: 16rem">
<div class="iv-stat iv-stat--card">
<span class="iv-stat__label">Invoiced (made-up figure)</span>
<strong class="iv-stat__value iv-count" data-iv-component="countup">€ 1,240.50</strong>
<span class="iv-stat__hint">English grouping, prefix kept</span>
</div>
<div class="iv-stat iv-stat--card" lang="es">
<span class="iv-stat__label" lang="en">Invoiced, Spanish locale (made-up figure)</span>
<strong class="iv-stat__value iv-count" data-iv-component="countup">12.480,50 €</strong>
<span class="iv-stat__hint" lang="en">Spanish grouping and decimal comma, suffix kept</span>
</div>
<div class="iv-stat iv-stat--card">
<span class="iv-stat__label">Coverage of the contract matrix</span>
<strong class="iv-stat__value iv-count" data-iv-component="countup" data-iv-autostart="true">98 %</strong>
<span class="iv-stat__hint">Starts at <code>init</code>, not in view</span>
</div>
<div class="iv-stat iv-stat--card">
<span class="iv-stat__label">Browser tests, three engines</span>
<strong class="iv-stat__value iv-count" data-iv-component="countup" data-iv-from="900" data-iv-duration="1800" data-iv-grouping="false">1068</strong>
<span class="iv-stat__hint">From 900, ungrouped while counting</span>
</div>
</div>
<p class="iv-u-mt-6">A figure also works inside a sentence: the last full gate
passed <strong class="iv-count" data-iv-component="countup" data-iv-duration="1200" data-iv-grouping="false">1068</strong> browser tests and
skipped <strong class="iv-count" data-iv-component="countup" data-iv-duration="1200">114</strong> of them, the visual
ones outside Chromium.</p>.iv-root {
--iv-stat-group-min: 16rem;
}// data-iv-component="countup"
{
autostart: true,
from: 900,
duration: 1800,
grouping: false,
}Options
| Option | Attribute | Default | Effect |
|---|---|---|---|
from | data-iv-from | 0 | Where the climb starts. A figure that only moves in its last digits reads faster from close by. |
duration | data-iv-duration | 900 | Milliseconds of the climb, eased out so it settles rather than stops. |
decimals | data-iv-decimals | -1 (auto) | Decimal places held during the climb. -1 is the sentinel for “take them from the served figure”, because the attribute grammar has no null. |
grouping | data-iv-grouping | true | Thousands separators while counting. Off, the digits stop shifting sideways. |
once | data-iv-once | true | Count the first time only. false counts again on every entry. |
autostart | data-iv-autostart | false | Start at init instead of waiting for the viewport. |
Methods and events
start(), reset(), destroy(); read value and done. value is the number currently shown, and done means “there is nothing left to count” — which is also true immediately under reduced motion or without an IntersectionObserver. destroy() puts back the nodes that were served, not just their text, so the DOM returns identical.
| Event | When | detail |
|---|---|---|
iv:count | the climb begins; cancelable | from, to |
iv:counted | the served figure is back on screen | from, to |
Where nothing counts, nothing is announced: neither event fires under reduced motion or without an observer.
Keyboard and assistive technology
A figure is text, not a control: there is nothing to focus and nothing to operate. It is deliberately not a live region — a number ticking into a screen reader forty times a second would be unusable — so assistive technology reads the final value, which is the one that was served and the one that is there again a second later. Give the figure a label in its own markup (.iv-stat__label does it inside a stat) and never let the number stand alone as a claim.
Without JavaScript
The page is already correct. The value is in the HTML, the styling is CSS, and .iv-count only adds font-variant-numeric: tabular-nums so the digits sit in fixed columns and the figure does not jitter while it climbs. Under prefers-reduced-motion: reduce, or where IntersectionObserver is missing, the component reads the figure and stops there.
Tokens and locals
Countup has no locals of its own: it borrows the typography of whatever holds it. Inside a stat that is --iv-stat-size, --iv-tracking-tight and the tabular figures of .iv-count; inside a sentence it is the surrounding text. The one rule the class enforces is the tabular numbers, and it is there for a visual reason, not a decorative one.
Common mistakes
- Serving
0and expecting the script to fill in the real number. Without JavaScript the page would then be a lie; serve the answer. - Counting a figure nobody measured. A number that climbs looks authoritative — say where it comes from, or mark it as invented.
- A long
durationon a figure above the fold: the reader arrives before the number does. - Grouping on while counting a four-digit number: the separator appears and disappears and the line jumps.
- Wrapping the figure in a live region to “make it accessible”. It makes it unreadable.