Count Up
stimeo--count-up
Animates a number up to the value authored in the markup — typically when it scrolls into view.
Adds the effect of a number running up to its final value, in markup alone. The final value stays written in the HTML, so search engines, visitors whose JavaScript never runs, and screen readers always read the real number. When the animation ends the original text comes back exactly as written, thousands separators and units included. What starts the run is yours to choose; the usual choice is "start when it scrolls into view". Visitors who ask for less motion never see the animation and get the final value straight away. Screen readers hear the final value too, never the numbers ticking past. A finished run is remembered, so coming back to the page does not count up again.
Scroll down until the stats come into view and the numbers run up. Spotting the moment and animating the number are separate controllers, combined by putting their attributes side by side. The final numbers stay written in the HTML, so the real values show even where JavaScript never runs.
- Users
- 12,480
- GitHub stars
- 3,200
- Uptime (%)
- 99
Keyboard
This component has no keyboard interactions of its own.
<%# count-up: the final numbers stay authored in the markup (SEO / no-JS / SR read
the real values); each stat composes intersection ("start when visible") with
count-up via data-action. Scroll the stats into view to run the animation. %>
<div class="count-up-demo">
<p class="count-up-demo__hint"><%= t("components.count_up.demo.hint") %></p>
<dl class="count-up-demo__stats">
<div class="count-up-demo__stat">
<dt><%= t("components.count_up.demo.stats.users") %></dt>
<dd data-controller="stimeo--intersection stimeo--count-up"
data-stimeo--intersection-once-value="true"
data-action="stimeo--intersection:enter->stimeo--count-up#start">12,480</dd>
</div>
<div class="count-up-demo__stat">
<dt><%= t("components.count_up.demo.stats.stars") %></dt>
<dd data-controller="stimeo--intersection stimeo--count-up"
data-stimeo--intersection-once-value="true"
data-action="stimeo--intersection:enter->stimeo--count-up#start">3,200</dd>
</div>
<div class="count-up-demo__stat">
<dt><%= t("components.count_up.demo.stats.uptime") %></dt>
<dd data-controller="stimeo--intersection stimeo--count-up"
data-stimeo--intersection-once-value="true"
data-action="stimeo--intersection:enter->stimeo--count-up#start">99</dd>
</div>
</dl>
</div>
/*
* Presentation-only styles for the count-up demo. The library animates the text
* content; this CSS only lays out the stat tiles.
*/
.count-up-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.count-up-demo__hint {
margin: 0;
color: var(--muted);
}
.count-up-demo__stats {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin: 0;
}
.count-up-demo__stat {
min-width: 10rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
background: var(--bg);
}
.count-up-demo__stat dt {
color: var(--muted);
font-size: 0.9rem;
}
.count-up-demo__stat dd {
margin: 0.25rem 0 0;
font-size: 1.75rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
This demo needs no consumer-side JS (the controller handles the behavior).
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--count-up"
Values
| Name | Description | Attribute |
|---|---|---|
duration
|
Animation length in ms. Default 1200. | data-stimeo--count-up-duration-value |
from
|
Starting number. Default 0. | data-stimeo--count-up-from-value |
once
|
Run only once (recorded via data-count-up-done). Default true. |
data-stimeo--count-up-once-value |
Actions
| Name | Description | Action |
|---|---|---|
start
|
Starts the animation (e.g. from stimeo--intersection:enter via data-action). |
stimeo--count-up#start |
Events
| Name | Description | Event |
|---|---|---|
end
|
Fires with { value } when the run settles (also under reduced motion). |
stimeo--count-up:end |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
data-count-up-done |
Controller element | Present after a finished run; suppresses re-runs while `once` is on. |
span[role=img][data-count-up-label] (while running) |
Wraps the ticking number | Names the animation with the authored final value, on an element that is allowed to carry a name. |