Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelog
  1. Home
  2. Blog
  3. Component guides
  4. React Animated Number Counter: Rolling Odometer Digits
Auf Deutsch lesen
  1. Blog
  2. Component guides
  3. React Animated Number Counter: Rolling Odometer Digits
Component guides
Component guides

React Animated Number Counter: Rolling Odometer Digits

SR
Serban Rusu · Founder of Wingo UI
Oct 19, 2026 · 9 min read

On this page

0%
  1. What should a React animated number counter handle?
  2. Count-up or odometer: which kind of animated number do you need?
  3. What props does the Animated Counter need?
    1. How do I style an animated counter with Tailwind?
  4. How do I format the number with Intl?
  5. How does a counter handle values that keep changing?
  6. How do I count up when the number scrolls into view?
  7. Is an animated counter accessible?
  8. When should I use the Stat Card instead?
  9. What are the common animated counter pitfalls?
  10. How do I add it to my project?
  11. FAQ
    1. How do I animate a number change in React?
    2. What is the difference between a count-up and an odometer counter?
    3. How do I make an animated counter accessible?
    4. Does an animated counter respect prefers-reduced-motion?
    5. Is the Wingo UI Animated Counter free?

TL;DR

A good React animated number counter rolls each digit on its own wheel from the number on screen, so fast updates spin instead of restarting, and it keeps tabular figures so the width only moves when a place is gained or lost. It should take its group and decimal marks from Intl, give screen readers one final value, and jump straight to the new number under reduced motion. The free Wingo UI Animated Counter does this from a single value prop; the Pro Stat Card wraps it in a dashboard tile.

Published Oct 19, 2026

You want a React animated number counter for a revenue total, a live visitor count or a pricing toggle. The first version is usually a setInterval that bumps a number in state until it reaches the target. Then the number changes again mid-count and restarts from zero, the text jitters left and right because a 1 is narrower than an 8, a German visitor sees 12,480.00 instead of 12.480,00, and a screen reader announces forty numbers in a row. This guide covers what a production counter needs, the props that matter in the Wingo UI Animated Counter, and how the Stat Card puts it in a dashboard tile.

What should a React animated number counter handle?

It should animate from the value on screen, hold its width, format with the locale, give screen readers one value and respect reduced motion. In detail:

  1. Animate from what is on screen. When you react to a number change mid-animation, the counter should retarget from the digits the user sees instead of starting over. Prices, scores and live counts change often, so this matters more than the first animation.
  2. A stable width. Tabular figures (font-variant-numeric: tabular-nums, tabular-nums in Tailwind) give every digit the same advance, so the number only grows when it gains a place.
  3. Locale formatting. Group and decimal marks differ by locale. Intl.NumberFormat already knows them, so the counter should ask it instead of hardcoding a comma.
  4. One value for screen readers. The moving digits are decoration. Assistive technology should get the final formatted number once.
  5. Reduced motion. Under prefers-reduced-motion: reduce the number should update in place, with no rolling.

Count-up or odometer: which kind of animated number do you need?

Use a count-up for a one-time reveal and an odometer for a value that keeps changing. Those are the two families of react animated numbers. A count-up tweens the whole value from A to B and prints every number in between, which suits a one-time reveal ("12,000 customers"). An odometer, or rolling counter, gives each digit its own vertical wheel and rolls only the digits that changed. That reads better for values that keep moving, because a price going from 1,249 to 1,251 moves two wheels instead of flashing through every number.

These are the options that come up when you search for a react odometer or a counter component. Facts are from each project's public docs and README, as of October 2026.

OptionStyleFormattingReduced motionPrice
react-countup (opens in a new tab)Count-up tween, wraps CountUp.js; odometer through a pluginseparator, decimals, prefix, suffix propsNot mentioned in the READMEFree, MIT
NumberFlow (opens in a new tab) (@number-flow/react)Rolling digitsformat takes Intl.NumberFormatOptions, locales an Intl localerespectMotionPreference, on by defaultFree, MIT
Motion AnimateNumber (opens in a new tab)Rolling digits, built on MotionIntl.NumberFormat (currency, compact)Not stated on the public pageMotion+ membership, one-time payment
Wingo UI Animated CounterRolling digits, built on MotionGroup and decimal marks from Intl via locale, or your own separatorsFollows prefers-reduced-motionFree, copied into your project as source

NumberFlow is the better pick when you need full Intl.NumberFormat output from the counter itself, such as compact notation (1.2K) or percent, because it formats with the whole options object. react-countup is fine for a single reveal on a landing page. The Animated Counter fits when you want the source in your repo, Tailwind classes, and the same tokens and spring presets as the rest of your UI.

What props does the Animated Counter need?

One: value. Everything else has a default that looks finished.

bash
npx wingo-ui@latest add animated-counter
tsx
"use client";
import { AnimatedCounter } from "@/components/ui/animated-counter";
export function Revenue({ total }: { total: number }) {
return (
<AnimatedCounter
value={total}
decimals={2}
size="lg"
weight="semibold"
prefix={<span className="mr-0.5">$</span>}
/>
);
}

The props you will change most:

  • decimals (default 0, up to 15): the value is rounded to that precision, so the wheels always land on a whole digit.
  • duration (default 0.6 seconds): roughly how long a digit takes to settle. Lower it for values that update every second.
  • bounce (default 0.18): spring overshoot of the wheels, from 0 to 1.
  • padStart (default 1): the fewest whole digits, padded with zeros, so a timer or a score holds its width when it crosses a power of ten.
  • prefix and suffix: a currency mark, a unit or an icon. They never animate, but they glide when the width changes.
  • fade and fadeSize (default true and 22): the soft fade at the top and bottom of each digit window. Turn fade off for a hard mechanical cut.

Drag along the ruler below. Digits roll up as the number grows and back down as it shrinks, and a place that appears slides in while the separators move with it.

Drag along the ruler, or focus it and use the arrow keys: each digit rolls on its own wheel, and a new thousands place slides in with its comma
$ npx wingo-ui@latest add animated-counter
FreeAnimated Counter docs

How do I style an animated counter with Tailwind?

Leave size, font and weight on their default inherit and the counter styles like text: a class on the counter or on its parent sizes it. A Tailwind animated counter then needs no extra props: a responsive size is just responsive classes.

tsx
<AnimatedCounter value={visitors} className="text-4xl font-semibold sm:text-6xl" />

The root already carries tabular-nums. Inside it, every digit column has data-slot="animated-counter-digit" and every separator data-slot="animated-counter-mark", so you can reach either with an arbitrary variant, for example [&_[data-slot=animated-counter-mark]]:opacity-60 for quieter commas. Use color (default currentColor) for a custom color; currentColor follows the text and works in light and dark mode without extra classes.

How do I format the number with Intl?

Pass locale. The counter calls Intl.NumberFormat(locale).formatToParts() once and takes the locale's group and decimal marks from it: en-US writes 1,234.5, de-DE 1.234,5 and fr-FR 1 234,5 with a narrow space. Set the locale once for the whole app with UIProvider and every counter picks it up.

tsx
import { UIProvider } from "@/lib/ui-config";
export function Providers({ children }: { children: React.ReactNode }) {
return <UIProvider locale="de-DE">{children}</UIProvider>;
}

An explicit separator or decimalSeparator always wins over the locale, which is how you turn grouping off for a year or an order number (separator=""). grouping="indian" places separators after the last three digits and then every two, 12,34,567.

Two limits to know. The counter takes only the marks from Intl, not the full formatted string, so a currency symbol is your prefix or suffix and its position is up to you. And it always draws the digits 0 to 9. If you need the symbol placed by locale, percent or compact notation, use the Stat Card below, which formats with Intl and switches compact and percent values to a count-up.

How does a counter handle values that keep changing?

Each wheel aims from where it is right now, which is what you want when you animate a number change in React for a value that never stops moving. If the value changes while a digit is still rolling, the spring retargets from the face on screen instead of finishing the old turn first, so a number that updates every second spins smoothly rather than stuttering. The direction follows the change: digits roll up when the value grows and down when it shrinks.

For a live value, shorten the duration so the wheels stay close to the real number:

tsx
<AnimatedCounter value={visitors} duration={0.4} weight="semibold" />

For a countdown, pad minutes and seconds to two digits and turn grouping off, so the width never moves:

tsx
<div className="flex items-baseline font-mono text-4xl font-semibold">
<AnimatedCounter value={Math.floor(secondsLeft / 60)} padStart={2} separator="" />
<span className="px-0.5">:</span>
<AnimatedCounter value={secondsLeft % 60} padStart={2} separator="" />
</div>

If you need days, hours and labels, the Pro Countdown component is built for that.

How do I count up when the number scrolls into view?

The counter shows its value on the first paint with no animation, which keeps server HTML and the first frame correct. To roll up on scroll, start at zero and pass the real value once the element is visible:

tsx
"use client";
import { useRef } from "react";
import { useInView } from "motion/react";
import { AnimatedCounter } from "@/components/ui/animated-counter";
export function CustomerCount({ total }: { total: number }) {
const ref = useRef<HTMLSpanElement>(null);
const seen = useInView(ref, { once: true, amount: 0.5 });
return (
<span ref={ref}>
<AnimatedCounter value={seen ? total : 0} size="xl" weight="bold" />
</span>
);
}

The trade-off: the server HTML now says 0, which is what crawlers and readers without JavaScript see. For a marketing number that matters for search, keep the real value in the copy next to it, or use the Stat Card, whose server HTML and first paint already show the final number and which counts up from zero only once it scrolls into view.

Is an animated counter accessible?

Yes, if the moving parts are hidden. The Animated Counter renders the rolling wheels with aria-hidden and puts the formatted number, minus sign included, in a visually hidden span next to them. The prefix and suffix stay outside the hidden part, so with a text $ prefix a screen reader reads "$12,480.00" once, never the faces that roll past. Give an icon prefix its own aria-hidden or a label.

Two rules on top of that:

  • Do not wrap a ticking number in aria-live. A visitor count that changes every two seconds would interrupt the user every two seconds. Announce only changes that matter, in your own polite live region, at a pace a person can follow.
  • Reduced motion means no roll. When the system asks for reduced motion, the counter sets each digit straight to its new face and resizes instantly, and places that appear or disappear switch without a transition. The hook behind it reads the media query after hydration, so the server and client render the same markup. The details of that pattern are in the prefers-reduced-motion in React guide.

The roll itself is a Motion spring (its visualDuration is duration, its bounce is bounce) on a motion value that drives transform, so React does not re-render per frame. If you want to tune the feel beyond bounce, the spring presets guide explains what bounce and visual duration do.

When should I use the Stat Card instead?

Use it when the number is a dashboard metric with a label, a change pill and maybe a sparkline. The Stat Card has three animation modes:

  • animation="count" (default): counts up from zero the first time the card scrolls into view (900ms, easing out), then from the previous value on later changes.
  • animation="roll": the Animated Counter odometer, for live numbers. It works for format="number" and format="currency"; the card places the currency symbol where the locale puts it. Other formats fall back to count.
  • animation="none": static.
tsx
import { Wallet } from "lucide-react";
import { StatCard } from "@/components/ui/stat-card";
export function CollectedToday({ amount }: { amount: number }) {
return (
<StatCard
label="Collected today"
icon={<Wallet />}
value={amount}
format="currency"
decimals={2}
animation="roll"
period="live"
locale="en-US"
/>
);
}

Screen readers hear one sentence with the final value and the change, such as "$128,450, up 12.4% vs. last month". Under reduced motion the number and the chart appear final at once.

Press Simulate a new day: the value rolls digit by digit to the new total and the delta updates
$ npx wingo-ui@latest add stat-card
ProStat Card docs

A full dashboard built from these tiles is in the Next.js admin dashboard tutorial.

What are the common animated counter pitfalls?

  • Proportional digits. Without tabular figures, 1,111 is narrower than 8,888 and the text shifts on every change. The Animated Counter sets tabular-nums and also sizes each column to its widest face, for fonts that ship without tabular figures.
  • Passing a string. value is a number. Format it with the props, not with toLocaleString() before you pass it in.
  • Huge or unsafe values. The counter caps the scaled value at Number.MAX_SAFE_INTEGER, because digits past that are noise. Keep money in cents on the server and divide for display.
  • Separators that fight the locale. If separator is passed, it wins over locale. Leave it out when you want the locale's marks.
  • A long number on a 360px phone. A text-7xl value with two decimals can overflow a narrow card. Use responsive size classes, or a smaller size inside cards.

How do I add it to my project?

Run npx wingo-ui@latest add animated-counter. The CLI copies the source into components/ui, adds the motion and ui-config helpers it imports, and installs motion. The Animated Counter is free; the Stat Card is a Pro component (pricing). More single-component deep dives are in the component guides category, and the React form components guide applies the same accessibility and mobile rules to every form control.

Components in this post

  • Animated Counter

    A React animated counter that rolls each digit to its new value like an odometer, for stats, prices, timers and live numbers.

    Free
  • Stat Card

    A React stat card for dashboards: a label, a big number that counts up, a delta pill that knows good from bad, a sparkline and a goal bar.

    Pro

FAQ

How do I animate a number change in React?

Keep the number in state and pass it to a component that animates between the old and new value, such as an odometer counter or a count-up tween. Animate transform and opacity only, and start each animation from the value currently on screen so rapid updates do not queue up.

What is the difference between a count-up and an odometer counter?

A count-up tweens the whole number through every value in between and re-renders the text on each frame. An odometer rolls each digit on its own wheel to the new face, so only the digits that changed move and the width stays stable.

How do I make an animated counter accessible?

Hide the moving digits from assistive technology and expose the formatted final value as plain text. Do not put a number that ticks every second inside an aria-live region, or a screen reader will read every update.

Does an animated counter respect prefers-reduced-motion?

Only if it checks the media query. The Wingo UI Animated Counter and NumberFlow both do by default; react-countup's README does not mention it, so you would check the preference yourself.

Is the Wingo UI Animated Counter free?

Yes. The Animated Counter is a free component you install with npx wingo-ui@latest add animated-counter. The Stat Card that uses it for dashboard tiles is a Pro component.

  • Animation
  • Numbers
  • Dashboard
  • Accessibility
  • React

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu founded Wingo UI. He builds its component library, CLI and MCP server, and writes about React interfaces for phones and AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Component guides

React Sparkline: Table Cells, KPI Tiles, Delta Chips

Add a React sparkline to table cells and KPI tiles with one scale across rows, a delta chip that knows good from bad, keyboard scrubbing and touch support.

SRSerban Rusu·Oct 19, 2026·9 min read
Comparisons and alternatives

Best React Component Library for Dashboard Apps in 2026

The best React component library for dashboard apps in 2026: 7 libraries compared on data tables, charts, filters and app shells, with prices and picks.

SRSerban Rusu·Oct 16, 2026·13 min read
Comparisons and alternatives

Magic UI Alternatives: 5 Animated React Libraries Compared

Magic UI alternatives compared in October 2026: Aceternity UI, React Bits, Cult UI, Motion Primitives and Wingo UI on license, price and reduced motion.

SRSerban Rusu·Oct 16, 2026·11 min read
Newsletter

Get new posts by email

New guides, tutorials and comparisons from the Wingo UI blog, sent when they are published.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components and blocks. Copy the source, make it yours, and let your coding agent build with it.

Product

  • Components
  • Templates
  • Pricing
  • AI agents
  • Theme generator

Resources

  • Docs
  • Blog
  • Changelog
  • Changelog RSS
  • llms.txt

Legal

  • Terms
  • Privacy
  • Refunds
  • License
  • Imprint

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities

© 2026 Wingo UI

Wingo UI