countup.js vs odometer
Animated Number Counters in Web Applications
countup.jsodometer

Animated Number Counters in Web Applications

countup.js and odometer are JavaScript libraries designed to animate numerical values on a webpage, often used for dashboards, statistics, or pricing displays. countup.js focuses on smoothly transitioning a number from a start value to an end value using requestAnimationFrame, offering high flexibility with formatting and easing. odometer creates a visual slot-machine effect by manipulating DOM elements to slide digits into place, providing a distinct aesthetic but requiring more structural setup.

Npm Package Weekly Downloads Trend

3 Years

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
countup.js08,1611.27 MB163 months agoMIT
odometer0---10 years agoMIT

countup.js vs odometer: Animation Engines Compared

Both countup.js and odometer solve the problem of animating numbers on a webpage, but they use different techniques to achieve the effect. countup.js updates the text content directly using JavaScript timing, while odometer manipulates the DOM to create a mechanical sliding animation. Let's compare how they handle initialization, animation, formatting, and maintenance.

🏗️ Initialization and Setup

countup.js requires a target element ID or reference and an end value. You can pass an options object to configure behavior like duration or easing.

import { CountUp } from 'countup.js';

// Initialize with target ID and end value
const countUp = new CountUp('targetElement', 2000);

// Start the animation
if (!countUp.error) {
  countUp.start();
} else {
  console.error(countUp.error);
}

odometer requires an element reference and a configuration object that includes the initial value. It often needs specific CSS themes to render correctly.

import Odometer from 'odometer';

// Initialize with element and options
const odometer = new Odometer({
  el: document.querySelector('#targetElement'),
  value: 2000,
  theme: 'minimal'
});

// Update value to trigger animation
odometer.update(3000);

🎬 Animation Mechanism

countup.js uses requestAnimationFrame to update the text content of the target element. It calculates the intermediate numbers in JavaScript and writes them to the DOM.

// countup.js updates textContent directly
// No extra DOM nodes are created inside the target
const countUp = new CountUp('target', 100, { duration: 2 });
countUp.start(); 
// Target innerHTML changes: "0" -> "50" -> "100"

odometer creates internal DOM structures to represent each digit. It slides these digit blocks up or down using CSS transitions to mimic a mechanical counter.

// odometer wraps digits in DOM elements for sliding effect
// Target innerHTML changes: 
// <div class="odometer"> -> <div class="odometer-digit">...</div>
const odometer = new Odometer({ el: document.querySelector('#target'), value: 100 });
// Adds multiple divs inside #target for each digit position

🎨 Formatting and Localization

countup.js has built-in options for prefixes, suffixes, and decimal places. You can configure these directly in the constructor options without extra plugins.

// countup.js: Built-in formatting
const countUp = new CountUp('target', 1234.56, {
  decimalPlaces: 2,
  prefix: '$',
  suffix: ' USD',
  useGrouping: true // Adds commas
});
countUp.start();

odometer relies on a format string property to define how numbers appear. It uses specific tokens like (ddd) for digits and , for grouping.

// odometer: Format string configuration
const odometer = new Odometer({
  el: document.querySelector('#target'),
  value: 1234.56,
  format: '(ddd).dd' // Defines digit structure
});
// Requires manual handling for prefixes/suffixes in DOM

🧩 Framework Integration

countup.js is framework-friendly because it does not alter the DOM structure unexpectedly. You can easily wrap it in React useEffect or Vue hooks.

// React example with countup.js
useEffect(() => {
  const countUp = new CountUp('counter', 100);
  countUp.start();
  return () => countUp.reset(); // Cleanup
}, []);

odometer can conflict with virtual DOM libraries because it injects its own HTML elements. You must manage the DOM carefully to prevent re-render issues in React or Vue.

// React example with odometer (requires caution)
useEffect(() => {
  const el = document.querySelector('#counter');
  const odometer = new Odometer({ el, value: 0 });
  odometer.update(100);
  // Cleanup is harder due to DOM manipulation
}, []);

🛠️ Maintenance and Long-Term Support

countup.js is actively maintained with regular updates to fix bugs and add features. It has no external dependencies, making it stable for long-term projects.

// countup.js: Modern ES6 module support
import { CountUp } from 'countup.js';
// Actively updated on npm and GitHub

odometer has seen very little activity in recent years and is considered legacy by many developers. It may require polyfills or specific CSS setups that are outdated.

// odometer: Legacy structure
import Odometer from 'odometer';
// Low maintenance activity; check repo before adopting

📊 Summary Table

Featurecountup.jsodometer
Animation StyleText update (JS driven)Sliding digits (DOM/CSS driven)
DOM ImpactLow (updates text)High (injects elements)
FormattingOptions objectFormat string tokens
DependenciesNoneRequires CSS themes
MaintenanceActiveLow / Legacy
Framework SafeYesCaution required

💡 Final Recommendation

countup.js is the standard choice for modern web development. It is lightweight, easy to control, and does not interfere with your layout or framework rendering. Use it for dashboards, pricing tables, or any scenario where you need a reliable number animation.

odometer should only be used if you specifically require its mechanical sliding visual and accept the risks of legacy software. For most new projects, the maintenance burden and DOM manipulation costs outweigh the visual benefits.

Final Thought: Both tools animate numbers, but countup.js offers a cleaner, more sustainable approach for today's frontend architecture.

How to Choose: countup.js vs odometer

  • countup.js:

    Choose countup.js for modern projects that require active maintenance, lightweight dependencies, and easy integration with frameworks like React or Vue. It is the safer long-term choice because it updates regularly and handles formatting logic without altering your DOM structure. This package works well when you need precise control over animation duration, easing, and callback events.

  • odometer:

    Choose odometer only if you specifically need its unique sliding-digit visual style and are working on a legacy project that already depends on it. Be aware that maintenance is low, and it modifies the DOM heavily by wrapping digits in extra elements, which can complicate CSS styling. It is less suitable for new applications where stability and framework compatibility are priorities.

README for countup.js

CountUp.js

CountUp.js is a dependency-free, lightweight Javascript class that animates a numerical value by counting to it.

Despite its name, CountUp can count in either direction, depending on the start and end values that you provide.

CountUp.js supports all browsers. MIT license.

Try the demo

Or tinker with CountUp in Stackblitz

Jump to:

Features

  • Auto-animate when element becomes visible. Use option autoAnimate = true.
  • Highly customizable with a large range of options, you can even substitute numerals.
  • Smart easing: CountUp intelligently defers easing to make it visually noticeable. Configurable in the options.
  • Plugins allow for alternate animations like the Odometer plugin

Odometer plugin

Usage:

Use CountUp with:

Use CountUp directly:

On npm as countup.js. You can import as a module, or include the UMD script and access CountUp as a global. See detailed instructions on including CountUp.

Params:

  • target: string | HTMLElement | HTMLInputElement - id of html element, input, svg text element, or DOM element reference where counting occurs.
  • endVal: number | null - the value you want to arrive at. Leave null to use the number in the target element.
  • options?: CountUpOptions - optional configuration object for fine-grain control

Options

OptionTypeDefaultDescription
startValnumber0Number to start at
decimalPlacesnumber0Number of decimal places
durationnumber2Animation duration in seconds
useGroupingbooleantrueExample: 1,000 vs 1000
useIndianSeparatorsbooleanfalseExample: 1,00,000 vs 100,000
useEasingbooleantrueEase animation
smartEasingThresholdnumber999Smooth easing for large numbers above this if useEasing
smartEasingAmountnumber333Amount to be eased for numbers above threshold
separatorstring','Grouping separator
decimalstring'.'Decimal character
easingFnfunctioneaseOutExpoEasing function for animation
formattingFnfunction—Custom function to format the result
prefixstring''Text prepended to result
suffixstring''Text appended to result
numeralsstring[]—Numeral glyph substitution
onCompleteCallbackfunction—Callback called when animation completes
onStartCallbackfunction—Callback called when animation starts
pluginCountUpPlugin—Plugin for alternate animations
autoAnimatebooleanfalseTrigger animation when target becomes visible
autoAnimateDelaynumber200Animation delay in ms after auto-animate triggers
autoAnimateOncebooleanfalseRun animation only once for auto-animate triggers
enableScrollSpyboolean—(deprecated) Use autoAnimate instead
scrollSpyDelaynumber—(deprecated) Use autoAnimateDelay instead
scrollSpyOnceboolean—(deprecated) Use autoAnimateOnce instead

Example usage:

const countUp = new CountUp('targetId', 5234);
if (!countUp.error) {
  countUp.start();
} else {
  console.error(countUp.error);
}

Pass options:

const countUp = new CountUp('targetId', 5234, options);

with optional complete callback:

const countUp = new CountUp('targetId', 5234, { onCompleteCallback: someMethod });

// or (passing fn to start will override options.onCompleteCallback)
countUp.start(someMethod);

// or
countUp.start(() => console.log('Complete!'));

Other methods:

Toggle pause/resume:

countUp.pauseResume();

Reset the animation:

countUp.reset();

Update the end value and animate:

countUp.update(989);

Destroy the instance (cancels animation, disconnects observers, clears callbacks):

countUp.onDestroy();

Auto animate when element becomes visible

Use the autoAnimate option to animate when the element is scrolled into view or appears on screen. When using autoAnimate, just initialize CountUp but do not call start().

const countUp = new CountUp('targetId', 989, { autoAnimate: true });

Note - Auto-animate uses IntersectionObserver which is broadly supported, but if you need to support some very old browsers, v2.9.0 and earlier use a window on-scroll handler when enableScrollSpy is set to true.


Alternate animations with plugins

Currently there's just one plugin, the Odometer Plugin.

To use a plugin, you'll need to first install the plugin package. Then you can include it and use the plugin option. See each plugin's docs for more detailed info.

const countUp = new CountUp('targetId', 5234, {
  plugin: new Odometer({ duration: 2.3, lastDigitDelay: 0 }),
  duration: 3.0
});

If you'd like to make your own plugin, see the docs below!

Tabular nums

To optimize the styling of counting number animations, you can take advantage of an OpenType feature called tabular nums which stabilizes jitteryness by using equal-width numbers.

In my experience, most OpenType fonts already use tabular nums, so this isn't needed. But it may help to add this style if they don't:

font-variant-numeric: tabular-nums;

Including CountUp

CountUp is distributed as an ES module, though a UMD module is also included, along with a separate requestAnimationFrame polyfill (see below).

For the examples below, first install CountUp:

npm i countup.js

Example with vanilla js

This is what is used in the demo. Checkout index.html and demo.js.

main.js:

import { CountUp } from './js/countUp.min.js';

window.onload = function() {
  var countUp = new CountUp('target', 2000);
  countUp.start();
}

Include in your html. Notice the type attribute:

<script src="./main.js" type="module"></script>

If you prefer not to use modules, use the nomodule script tag to include separate scripts:

<script nomodule src="js/countUp.umd.js"></script>
<script nomodule src="js/main-for-legacy.js"></script>

To run module-enabled scripts locally, you'll need a simple local server setup like this (test the demo locally by running npm run serve) because otherwise you may see a CORS error when your browser tries to load the script as a module.

For Webpack and other build systems

Import from the package, instead of the file location:

import { CountUp } from 'countup.js';

UMD module

CountUp is also wrapped as a UMD module in ./dist/countUp.umd.js and it exposes CountUp as a global variable on the window scope. To use it, include countUp.umd.js in a script tag, and invoke it like so:

var numAnim = new countUp.CountUp('myTarget', 2000);
numAnim.start()

Contributing

Before you make a pull request, please be sure to follow these instructions:

  1. Do your work on src/countUp.ts
  2. Lint: npm run lint
  3. Run tests: npm t
  4. Build and serve the demo by running npm start then check the demo to make sure it counts.

Creating Animation Plugins

CountUp supports plugins as of v2.6.0. Plugins implement their own render method to display each frame's formatted value. A class instance or object can be passed to the plugin property of CountUpOptions, and the plugin's render method will be called instead of CountUp's.

export declare interface CountUpPlugin {
  render(elem: HTMLElement, formatted: string): void;
}

An example of a plugin:

export class SomePlugin implements CountUpPlugin {
  // ...some properties here

  constructor(options: SomePluginOptions) {
    // ...setup code here if you need it
  }

  render(elem: HTMLElement, formatted: string): void {
    // render DOM here
  }
}

If you make a plugin, be sure to create a PR to add it to this README!