beta
Number Ticker — <mv-number-ticker>
Animated counter formatted with Intl (en-US by default: currency, percent, compact, decimals) in two modes: smooth interpolation or rolling odometer; re-animates on every value change, with no layout shift.
| Category | Animated text |
|---|---|
| Type | Web Component (<mv-number-ticker>) |
| Status | beta |
| Keywords | counter, count-up, odometer, stats, kpi, animated-number, slot |
When to use
- A KPI, price or stat should count up to its value when scrolled into view
- A live-updating value needs an odometer-style roll with no layout shift
- Currency, percent or compact numbers must animate with locale-aware formatting
Avoid when
- A complete key-figures section is needed rather than a single number → use Stats instead
- A signature dashboard number should burst into particles with up/down flashes → use Particle Counter instead
- Dense data tables where many numbers would animate at once and distract
Install
node scripts/add.mjs number-ticker --out ./src/marvelousAI agent with the Marvelous UI MCP server: install_components({ slugs: ["number-ticker"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, core/observe.js, components/number-ticker/number-ticker.js, components/number-ticker/number-ticker.css.
Usage
Canonical markup — start from it and customize with attributes, data-* and CSS variables:
<div id="nt-demo" style="display:grid;gap:1.5rem;width:min(100%,48rem);margin-inline:auto">
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(10rem,1fr));gap:.75rem">
<div style="padding:1rem 1.1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface)">
<div style="font:700 2rem/1.1 var(--mv-font-display);letter-spacing:-.03em"><mv-number-ticker value="12840" prefix="+"></mv-number-ticker></div>
<div style="margin-top:.25rem;font-size:.85rem;color:var(--mv-fg-muted)">customer teams</div>
</div>
<div style="padding:1rem 1.1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface)">
<div style="font:700 2rem/1.1 var(--mv-font-display);letter-spacing:-.03em"><mv-number-ticker value="4.9" suffix="/5" delay="150"></mv-number-ticker></div>
<div style="margin-top:.25rem;font-size:.85rem;color:var(--mv-fg-muted)">average rating</div>
</div>
<div style="padding:1rem 1.1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface)">
<div style="font:700 2rem/1.1 var(--mv-font-display);letter-spacing:-.03em"><mv-number-ticker value="99.98" format="percent" delay="300" easing="var(--mv-ease-in-out)"></mv-number-ticker></div>
<div style="margin-top:.25rem;font-size:.85rem;color:var(--mv-fg-muted)">uptime</div>
</div>
<div style="padding:1rem 1.1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface)">
<div style="font:700 2rem/1.1 var(--mv-font-display);letter-spacing:-.03em"><mv-number-ticker value="2400000" format="currency" compact delay="450"></mv-number-ticker></div>
<div style="margin-top:.25rem;font-size:.85rem;color:var(--mv-fg-muted)">raised in seed</div>
</div>
</div>
<div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:1rem;padding:1.25rem 1.4rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);background:linear-gradient(to bottom,var(--mv-surface-raised),var(--mv-bg-subtle));box-shadow:var(--mv-shadow-sm)">
<div>
<div style="font-size:.8rem;letter-spacing:.08em;text-transform:uppercase;color:var(--mv-fg-muted)">Today’s revenue</div>
<div style="margin-top:.2rem;font:800 clamp(2.2rem,6vw,3.4rem)/1.1 var(--mv-font-display);letter-spacing:-.04em">
<mv-number-ticker id="nt-demo-live" mode="roll" value="48265.90" format="currency"></mv-number-ticker>
</div>
</div>
<button class="mv-button" id="nt-demo-sale">+ Simulate a sale</button>
</div>
</div>
<script type="module">
const live = document.getElementById("nt-demo-live");
document.getElementById("nt-demo-sale").addEventListener("click", () => {
const next = Number(live.value) + Math.round(40 + Math.random() * 900) + Math.round(Math.random() * 99) / 100;
live.value = next.toFixed(2);
});
</script>API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
value | number | 0 | Target value. Changing it restarts the animation from the displayed value. |
from | number | 0 | Starting value of the first animation. |
mode | count | roll | count | count = interpolation; roll = odometer (each digit rolls vertically). |
format | decimal | currency | percent | compact | decimal | Intl style. percent expects percentage points (42 → 42%). |
compact | boolean | Compact notation, combinable (e.g. currency + compact → $2.4M). | |
currency | ISO 4217 code | USD | Currency for format=currency. |
locale | BCP 47 | en-US | Formatting locale (separators, symbols). |
decimals | number | (inferred from value/from) | Number of decimals shown. |
duration | number (ms) | 2000 (count) / 1400 (roll) | Animation duration. |
delay | number (ms) | 0 | Delay before the first animation. |
stagger | number (ms) | 60 | Offset between reels (roll mode). |
easing | CSS easing | var(--mv-ease-out) | Any CSS curve, including var(--mv-ease-spring) (linear()). |
trigger | view | load | manual | view | What starts the first animation. manual waits for replay(). |
prefix / suffix | string | Fixed text around the number (e.g. +, /5). |
Methods
| Name | Description |
|---|---|
replay() | Replays from from to value. |
Events
| Name | Description |
|---|---|
mv-end | Animation finished (detail.value). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-number-ticker-align | end | Alignment of the number within its reserved width (count mode). |
--mv-number-ticker-fade | 0.14em | Height of the top/bottom fade on the reels. |
Accessibility
The final formatted value (prefix and suffix included) lives in a hidden copy; the animated digits are aria-hidden, so they are never read out in bursts. Tabular digits and reserved width: no jitter. With reduced motion, the final value shows immediately.