Odometer.js Tutorial

Odometer js is a tiny, focused library for a rolling number counter. Digits glide into place like a mechanical dashboard—great for KPIs, stats, and counters that deserve a little drama. This odometer js tutorial walks you through setup, initialization, scroll triggers, and theming, with working odometer.js examples you can paste into your project.

When should you prefer a rolling effect over a simple count-up? Use Odometer when motion matters: launch numbers, fundraising totals, or any stat that benefits from tactile feedback. If you only need a fade or easing count, see our guide to alternatives like CountUp in the companion article CountUp.js quickstart or the broader number‑counter animation patterns.

Built and maintained by HubSpot, the library is well-documented and stable. You’ll find official details at the Odometer.js docs. If you want a jQuery-style plugin approach, there’s also Counter-Up, but Odometer’s signature is the rolling digits.

Below, you’ll add an odometer theme, initialize counters, format numbers, and trigger them on scroll—no fuss, just what you need.

Overview

Odometer’s value is in deliberate motion. It’s simple to wire up, light on dependencies, and designed for readability.

  • What it does: Rolling number counter with animated digit wheels.
  • Why it’s different: Emulates an odometer—not a tweened text change—so numbers feel physical.
  • When to use it: KPIs, launch metrics, live stats, dashboards, and hero sections.
  • What you get: Themes, formatting (decimals and thousands), easy programmatic updates, and a clean API.
  • Where it comes from: A HubSpot open‑source project (often called the hubspot odometer).

If you need pure numeric animation without the rolling effect, compare with our other guides linked above.

Add Odometer to your site instantly with MicroEdits

If you’d rather not touch code, MicroEdits can add Odometer to your existing site for you. Just describe the change in plain English. MicroEdits injects the right CSS and JS, sets the odometer theme, binds your counters, and previews everything before anything goes live.

  • No coding: Say what you want; MicroEdits writes and applies the change.
  • Instant previews: See the rolling number counter, share it, and revert anytime.
  • Works anywhere: WordPress, Shopify, custom stacks—if it’s a website, it works.
  • Third‑party friendly: It can also wire up tools like Google Analytics, Calendly, or Hotjar alongside your counters.

To get started, enter your site URL and ask for a rolling number counter in your hero, or request an odometer theme swap. It just works.

enter any\nwebsite

Setup (CDN or local)

You can add Odometer via a CDN or install it locally. The CDN path is the quickest way to see results.

CDN quick start

  • Include an odometer theme stylesheet in your page.
  • Load the Odometer JS file from a CDN like cdnjs.
  • Add a container with the class odometer.
<!-- Head: theme stylesheet -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/odometer.js/0.4.8/themes/odometer-theme-minimal.css" />

<!-- Body: your counter element -->
<div class="odometer" id="visitors">0</div>

Note: Place the Odometer JS file near the end of your document so it runs after the DOM is ready. See the official Odometer docs for the current file paths and options.

Local or npm

Prefer to ship it yourself?

npm install odometer

Then include a theme CSS in your bundle and reference Odometer in your app code. Bundlers vary; see the Troubleshooting section for module import tips.

Minimal HTML structure

Keep your markup simple. One element per counter is enough.

<div class="odometer" id="revenue">0</div>
<div class="odometer" id="subscribers">0</div>

Initialization and options

You can let Odometer auto‑initialize (recommended for most cases) or manually initialize it.

Option A: Auto‑initialize (simplest)

If an element has the class odometer and you change its innerHTML to a number, Odometer will roll the digits automatically.

// Global defaults (optional)
window.odometerOptions = {
  format: '(,ddd).dd',   // thousands + 2 decimals
  duration: 1200,        // in ms
  theme: 'minimal'
};

// Later in your code:
const el = document.querySelector('#revenue');
el.innerHTML = 0;
setTimeout(() => {
  el.innerHTML = 12847.56;  // Triggers the rolling number counter
}, 800);

This is the fastest way to get moving numbers with formatted output. The format string supports thousands and decimals; the example shows the required pattern, including the often‑used format ’(,ddd).dd’.

Option B: Manual initialize

If you want explicit control, instantiate an odometer instance.

const od = new Odometer({
  el: document.getElementById('subscribers'),
  value: 0,
  format: '(,ddd)',
  duration: 1000,
  theme: 'minimal'
});

// Programmatic updates
od.update(3500);
setTimeout(() => od.update(12000), 1200);

Both approaches are fine. Pick one to avoid double initialization.

Common options at a glance

  • value: starting number.
  • format: number formatting, e.g., (ddd), (ddd).dd, or (,ddd).dd.
  • duration: animation time in milliseconds.
  • theme: odometer theme name, such as minimal or default.

Triggering on scroll

Counters should move when they’re visible—not when the page loads. The IntersectionObserver API is the clean, performant choice for a single‑run trigger.

<!-- Example markup with target values -->
<div class="odometer" data-count-to="37500" id="sales">0</div>
<div class="odometer" data-count-to="12847.56" id="profit">0</div>
// Single-run IntersectionObserver
const targets = document.querySelectorAll('.odometer[data-count-to]');

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (!entry.isIntersecting) return;

    const el = entry.target;
    const to = parseFloat(el.getAttribute('data-count-to') || '0');

    // Auto-initialize path: updating innerHTML triggers the roll
    el.innerHTML = to;

    // Stop observing after the first run
    observer.unobserve(el);
  });
}, {
  threshold: 0.3
});

// Observe all counters
targets.forEach((el) => observer.observe(el));

Why this pattern works:

  • It’s a single observer for many counters.
  • It runs once per element, then unobserves to prevent replays.
  • No scroll event spam, no debounce needed.

Learn more about the API at MDN IntersectionObserver.

Styling and theming

Pick an odometer theme, then fine‑tune typography and spacing for your design. You can override the theme’s variables with your own CSS.

Theme options

Theme nameFile (on CDN or local)Visual style
defaultodometer-theme-default.cssBalanced, subtle shadows
minimalodometer-theme-minimal.cssClean, low‑contrast
carodometer-theme-car.cssClassic gauge aesthetic
digitalodometer-theme-digital.cssSeven‑segment vibe
plazaodometer-theme-plaza.cssBold, display‑style
slot-machineodometer-theme-slot-machine.cssPlayful reels
train-stationodometer-theme-train-station.cssIndustrial, high‑contrast

Switching themes is as simple as changing the stylesheet you include. You can also override colors, spacing, and fonts.

Typography and font performance

Use a typeface with tabular numerals so each digit has equal width. That prevents jitter as digits slide.

.odometer,
.odometer .odometer-inside {
  font-feature-settings: 'tnum' 1, 'lnum' 1;
  font-variant-numeric: tabular-nums lining-nums;
  font-size: clamp(1.5rem, 3vw, 3rem);
  line-height: 1.1;
}

If you self‑host fonts, load them early and use a sensible display strategy to avoid swap jank. See MDN for font‑variant‑numeric details.

Spacing and layout

Keep enough width for longer numbers and separators. If digits appear clipped, check parent containers for overflow rules or transforms. A little padding goes a long way.

.stats {
  display: grid;
  gap: 1rem;
}
.stats .odometer {
  padding: 0.25rem 0.5rem;
  white-space: nowrap;
}

Troubleshooting

  • Double initialization: Don’t combine auto‑init (class odometer + innerHTML updates) with manual new Odometer on the same element. Choose one. If you need manual control, remove the odometer class or avoid innerHTML changes.
  • CSS conflicts: Clipped digits or broken stacking often come from overflow: hidden, transform/translate on parents, or tight line-height. Relax those constraints or wrap the odometer in a container with natural flow.
  • Formatting surprises: The format string is literal. Use (,ddd).dd for thousands and two decimals. If your input is a string, parse it to a number before updating.
  • SSR frameworks: In Next.js, Nuxt, or Remix, run initialization client‑side only (e.g., in an effect or after mount). Don’t access window or document on the server.
  • Module bundlers: The package may expose a UMD build. If a direct import fails, load it as a side effect and reference window.Odometer. Verify your bundler’s config or fall back to the odometer js cdn.
  • Scroll not firing: For IntersectionObserver, ensure the target exists, thresholds are sane, and no ancestor has overflow clipping the viewport. Test with thresholds around 0.1–0.3.

FAQ

Can Odometer show decimals and thousands separators?

Yes. Use the format option to control grouping and precision. A common pattern is (ddd) for plain thousands, (,ddd) for comma separators, and (.ddd) for dot grouping. Append .dd to show two decimals. For example, format: ’(,ddd).dd’ yields 12,847.56. If your values arrive as strings, convert them with Number() before updating to ensure correct formatting and smooth animation.

How do I update values dynamically from an API?

Update the innerHTML of a class odometer element, or call instance.update(newValue) if you manually initialized. Debounce or throttle API calls so you don’t spam animations. For dashboards, update on an interval (e.g., every 15–60 seconds) and only animate when the value changes. If you use scroll triggers, guard against updating while the element is off‑screen unless you intend a jump.

What’s the fastest way to initialize—auto or manual?

Auto‑init is the quickest: add the odometer class and set innerHTML to your target number when you’re ready. Manual initialize is useful if you want explicit control over value, duration, or multiple instances at once. Pick one approach and stick with it to prevent double init. Both support themes, formatting, and programmatic updates.

How do I trigger the rolling number counter on scroll only once?

Use an IntersectionObserver, observe the odometer elements, set innerHTML to the target value when entry.isIntersecting, then unobserve that element. This yields a single run without scroll event handlers. A threshold around 0.3 works well for most layouts. If your page uses sticky headers, adjust rootMargin to trigger a bit earlier to avoid late animations.

Does Odometer require jQuery or any framework?

No. Odometer is framework‑agnostic and works with plain JavaScript. It also plays well in React, Vue, or Svelte: initialize after mount, and avoid re‑mounting the node during animation. In React, keep the element stable and update values in an effect so Odometer can manage the DOM it owns without reconciliation fights.

Can I run multiple counters with different themes?

Yes. Include multiple theme stylesheets or scope overrides. You can wrap specific counters in a container that applies a different theme class. For example, one section can use odometer-theme-minimal while another uses odometer-theme-digital. Keep font choices consistent to avoid layout shifts when switching styles across sections.

What’s a good duration for performance and feel?

Between 800–1500 ms covers most use cases. Shorter feels snappy; longer risks sluggishness, especially on mobile. Reserve long durations for marquee numbers in hero sections. If you animate many counters at once, stagger them by 100–200 ms for perceived performance and easier reading.


Looking for broader patterns and alternatives? See our complete number‑counter animation guide and the CountUp.js quickstart for non‑rolling styles. Prefer a jQuery‑style approach? Check Counter‑Up integration.