Data✨ Premium

Counter Odometer

Odometer counter: each digit rolls inside its cell to its place when the counter appears, then on every new value.

CSSTransformCounter

Updated

This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Data has 28 effects, including 1 free. Explore the category →

3 usage examples

Visitors since the reopening — counter at the top of a sewing museum's home page — Counter Odometer example 1

① Visitors since the reopening — counter at the top of a sewing museum's home page

WhenUnder the home page title, the counter shows 12,487 visitors in navy cells; with each recorded entry, the last digit rolls one notch and the carry turns the neighbouring cells (12,489 → 12,490).
WhyA small museum has little proof of its attendance: a mechanical counter moving while the page is read shows the place is alive, in the style of an old object that suits the collections.
Settings--odom-w:40px, --odom-h:54px, --odom-size:1.65rem, navy cells (--odom-cell:#1d2340, --odom-border:#1d2340), cream digits --odom-color:#f7f1e6, transparent background, left-aligned. In the capture, a scene timer adds 1 to data-value every 1.4 s; on a real site, the value comes from the ticketing system.
Total mileage after syncing — screen of a bike computer app — Counter Odometer example 2

② Total mileage after syncing — screen of a bike computer app

WhenThe rider opens the app and taps "Sync": the morning ride is fetched and the total rolls from 4,812 to 4,849 km, then to 4,861 with a second ride.
WhyA bike's total reads like a car's: finding the gesture of a turning counter makes syncing concrete and shows what was just added.
Settings--odom-w:34px, --odom-h:46px, --odom-size:1.4rem, acid-green digits --odom-color:#a3e635 in #1f2937 cells. The button writes the new sum into data-value half a second after the tap; in the capture, a click anywhere in the scene does the same.
Score of a word game — small counter in the corner of the board — Counter Odometer example 3

③ Score of a word game — small counter in the corner of the board

WhenThe player lays ORANGE on the board and taps "Play the word": the 12 points join the score in the corner, which rolls from 1,248 to 1,260, and the word joins the list.
WhyThe score is what players watch from the corner of their eye: the roll draws the gaze just enough at the moment of the gain, without covering the board.
SettingsSmall cells: --odom-w:26px, --odom-h:34px, --odom-size:1.1rem, #fde68a digits on #14532d cells, gap reduced to 3 px by the scene CSS. "Play the word" adds 12 to data-value; in the capture, the first click in the scene plays the word.

How it works

The HTML comes down to <div class="odom-counter" data-value="12487">12,487</div>. The script formats the value in the page language ("12,487" in English, "12 487" in French), then builds one .odom-cell per digit holding a vertical 0-to-9 strip (.odom-strip), and a narrower cell for each separator (.odom-sep). All strips start on 0.

When the counter is half visible (IntersectionObserver, once), each strip slides by 10% of its height per digit, over 0.8 s with a strong ease-out, each one 120 ms after the previous (first start at 300 ms). Writing a new value to data-value later rolls all cells together to the new digits; if the number of digits changes (9,999 → 10,000), the cells are rebuilt and roll from 0.

The value is set only through data-value, from your HTML or your code; the component shows no button. Each counter is independent and nothing is declared on window.

Accessibility

  • One number, not fifty digits: the strips are hidden (aria-hidden="true") and an invisible .odom-sr text holds the formatted number. Screen readers read "12,487", updated on every new data-value.
  • Context: the counter has no label of its own; put a text next to it ("visitors since 2019") so it means something.
  • Reduced motion: under prefers-reduced-motion: reduce, the transition is removed; digits are in place as soon as they appear and change without rolling.
  • Announcing changes: the counter is not a live region. If a new value must be announced, put aria-live="polite" on a message of your own rather than on the counter, which would change too often.

Browser compatibility

Dependency-free JavaScript (about 3 KB), CSS transitions and CSS variables. Measured on 08/11/2026 in Chromium 151, Firefox 153 and WebKit 26.5 (Playwright): roll on appearance up to 12487, a counter further down waiting for the scroll, a new data-value (5021) on the 2nd copy without touching the 1st, read text "12,487", 390 px, reduced motion, no error. Versions taken from @mdn/browser-compat-data: IntersectionObserver sets the minimum.

Chrome 51+✓ Full (measured)
Firefox 55+✓ Full (measured)
Safari 12.1+✓ Full (measured in WebKit)
Edge 79+✓ Full (Chromium engine)
iOS Safari 12.2+✓ Not checked on a real device
Android Chrome 51+✓ Full (Chromium engine)

Without IntersectionObserver, the digits are set on load. Without JavaScript, the HTML text ("12,487") shows plainly, without cells.

The code

HTML structure to paste into your page (the CSS + JS come with your purchase):

index.html — structure
<div class="fx-0241-root">
  <div class="odom-counter" data-value="12487">12,487</div>
</div>
<!-- built by the script inside .odom-counter: -->
<span class="odom-sr">12,487</span>
<span class="odom-row" aria-hidden="true">
  <span class="odom-cell"><span class="odom-strip"><span>0</span> … <span>9</span></span></span>
  <span class="odom-sep">,</span>
  …
</span>

One-time payment, no subscription — the code is yours forever

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Value (HTML — data-value on .odom-counter) 12487 Positive integer. Change it later in JavaScript (counter.dataset.value = 12500): the cells roll to the new value.
Starting text (HTML — content of .odom-counter) 12,487 Shown only without JavaScript; keep it equal to data-value.
Cell size (CSS — --odom-w, --odom-h, --odom-size) 44px · 60px · 1.8rem Cell width, cell height and digit size; shrink all three together for a small counter.
Colours (CSS — --odom-color, --odom-cell, --odom-border, --odom-sep) #a78bfa · indigo at 15% · indigo at 30% · white at 30% Digits, cell background and border, separator colour.
Speed (CSS — --odom-duration) .8s Roll time of each cell. The 120 ms offset between cells on first display is in the script.
Separator (HTML — lang attribute) the page's Comma in English, thin space in French. Set lang on the root to force one.
Background and height (CSS — --odom-bg, --odom-height) #0a0a0f · 300px Inside a page: --odom-bg:transparent; --odom-height:0px.

FAQ

No: the script rounds data-value to the nearest whole number and turns a negative number into 0, since each cell only rolls from 0 to 9. For a temperature or a balance, keep a plain text display.
Not directly: the value is formatted as a number. Write the number of digits you want in your own text next to it, or adapt the formatting line (Intl.NumberFormat, minimumIntegerDigits option).
Yes. Each new data-value starts the roll again from the current position; with a 0.8 s cell, leave at least one second between two values so the roll can finish.