Count Up
stimeo--count-up
Animates a number up to the value authored in the markup — typically when it scrolls into view.
The stimeo--count-up controller animates a number from `from` up to the value already written in the DOM, restoring the authored text (separators and suffixes included) verbatim when it settles — so SEO, no-JS, and screen readers always have the real value. Start it any way you like via the start action; the classic trigger is the intersection primitive ("animate when visible"), composed in markup with data-action="stimeo--intersection:enter->stimeo--count-up#start". Under prefers-reduced-motion the animation is skipped entirely, and while running the authored value is kept in aria-label so assistive tech never hears the intermediate numbers. With once (default) a finished run is recorded as data-count-up-done and honored across Turbo cache restores.
Scroll the stats into view — each composes intersection (the trigger) with count-up (the animation). The final numbers stay authored in the markup.
- Users
- 12,480
- GitHub stars
- 3,200
- Uptime (%)
- 99
<%# 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. |
aria-label (while running) |
Controller element | Holds the authored final value for assistive tech during the animation. |