Count Up

Counts a number up to its value when it scrolls into view.

zweiundeins <sb-count-up> number counter animation statistics intl Since 2026-09-23 967 B0Open in playground Edit on GitHub

Playground

Counts a number to its value the first time it is on screen: for the statistics on a landing page, a total that just changed, anything that should arrive rather than appear. It takes its font, size and colour from where you put it.

Put the server-formatted number inside it. That is what search engines and people without JavaScript see, and screen readers always hear the final value rather than the frames of the count.

Examples

On scroll

1113 models, 56 makes.

<p style="font-size: 1.25rem">
  <sb-count-up value="1113" style="font-size: 2.5rem; font-weight: 700">1113</sb-count-up>
  models, <sb-count-up value="56">56</sb-count-up> makes.
</p>

Decimals and units

decimals is kept for every frame, so the width does not change while it counts. Units stay outside the element.

CHF 0.52 / km

<p style="font-size: 2rem">
  CHF <sb-count-up value="0.52" decimals="2" duration="2000">0.52</sb-count-up> / km
</p>

A new value

When the server sends a new value while the number is on screen, it counts from where it stands.

1200 Add an order
<div data-signals="{_total: 1200}" style="display: flex; gap: 1rem; align-items: center">
  <sb-count-up data-attr:value="$_total" data-preserve-attr="value" style="font-size: 2rem">1200</sb-count-up>
  <sb-button data-on:click="$_total += Math.round(Math.random() * 500)">Add an order</sb-button>
</div>

Accessibility

Screen readers get the final value as text; the counting digits are hidden from them. With prefers-reduced-motion: reduce the number is shown at its value straight away, and a new value replaces it without counting.

Installation

Add Datastar with Rocket and the Starbase autoloader once per page, then use the tag. The autoloader imports each component the first time its tag appears, including tags added later by a Datastar morph.

<!-- Once per page: Datastar with Rocket, and the Starbase autoloader.
     It loads every <sb-…> component the first time its tag appears. -->
<script type="importmap">
  { "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>
<!-- Optional, no flash of undefined elements: class="sb-cloak" on <html>, and -->
<style>.sb-cloak :not(:defined) { visibility: hidden }</style>

<sb-count-up value="4681" style="font-size: 2rem">4681</sb-count-up>

<!-- In production, pin today's catalog instead of the latest: the browser then
     refuses any file that changed. Add "integrity" to the import map above: the
     hashes from https://starbase.zweiundeins.gmbh/c/@722e189276ac/importmap.json and Datastar's, below. -->
<!--
<script type="importmap">
  { "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" },
    "integrity": { "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js": "sha384-vUxZojLrF1Ar3de5h7VINqhJXBgjyZS4U49pHvGa87kum6j5Xn6JRziuARNw5ELG", "…": "…from importmap.json" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@722e189276ac/autoloader.js" integrity="sha384-scFa7/4jztDOx1YZT9/1g1ULYm9LsDlQn/8hMTyyD4IkPnZV2pfhHytoNTWTJG6b"></script>
-->

<!-- Or load just this component, pinned to this version. The minified module
     is what the autoloader uses; the readable source is the same URL without .min. -->
<!-- <script type="module" src="https://starbase.zweiundeins.gmbh/c/count-up@2aadfff22690/count-up.min.js" integrity="sha384-yjJQfnl3zFj2W7ghTEmUGV5VaL4Iah+86kkTayK2Cp5ctiULI4XzbJXLpGY5hJ2Y"></script> -->

Size

Each file compressed on its own, the way it is served (gzip -9, brotli -11). The autoloader loads the minified files (esbuild), so that last column is what a page downloads; the readable source is always there too. Datastar and Rocket are shared by every component and not counted.

FileOriginalgzipbrotliminified
count-up.js 3.7 kB1.7 kB1.4 kB967 B

API reference

Props

AttributeTypeDefaultDescription
valuenumber0The number to arrive at.
fromnumber0Where the count starts the first time it is seen.
decimalsnumber0Fixed digits after the decimal mark, kept for every frame so the width stays put.
durationnumber1400Length of the count in milliseconds.
groupingbooleantrueGroup thousands the way the locale does.
langstring""Locale for the separators (default: the page's lang, then the browser's).