CountUp.js Tutorial

Animated numbers do one job: make your metrics feel alive. With count up js you can turn a static “12,457” into a smooth count up number animation that lands on the final value with precision. This guide gives you a fast path from zero to polished counters, including formatting, scroll triggers, and a tidy setup that avoids common pitfalls. If you prefer a rolling “odometer” look, see our comparison in the Odometer guide at rolling number counters with Odometer.js, or browse the broader number counter animation techniques.

CountUp.js is small, focused, and flexible. It doesn’t care whether you’re on WordPress, Shopify, or a custom stack. Below you’ll find concise countup js examples, a clear set of countup options, and an IntersectionObserver countup pattern that starts only when the user actually sees the number.

Overview

CountUp.js solves three problems at once:

  • Displays the right value: You set the target number and duration; it lands precisely.
  • Formats like a pro: Decimals, separators, currency prefix/suffix—no fuss.
  • Triggers sensibly: Start on page load or on scroll to avoid wasted animation.

Ideal use cases:

  • Hero stats and KPI panels
  • Pricing totals, revenue, or other currency values
  • Dashboards or summaries where precision and readability matter

For a jQuery-first site, consider the alternative in Counter‑Up for jQuery users, but CountUp is framework‑agnostic and runs anywhere.

Add CountUp.js to your site with MicroEdits

Most people don’t want to wrangle build tools or dig through templates to add a simple animation. MicroEdits lets you say what you want in plain English and see it happen on your existing site. No coding, no hunting through theme files.

Tell MicroEdits what you need—something like Add CountUp.js via a CDN and animate all elements with the class .stat from 0 to their data-target. It loads the library, wires up the logic, and shows you a live preview. Changes can be shared, published, or reverted instantly. It works on any website or platform because MicroEdits applies the changes directly to your site.

  • No coding required: Describe the change; MicroEdits does the rest.
  • Instant results: Preview and publish in one flow.
  • Works everywhere: WordPress, Shopify, Webflow, custom sites—no special setup.

enter any website

Quick start

Here’s a minimal setup you can adapt. If you’re using a countup cdn, load the UMD build of CountUp.js from a trusted source (for example, jsDelivr or UNPKG; see the project README at CountUp.js), then run the code below.

HTML targets:

<span id="visitors">0</span> <span id="revenue">0</span>

Minimal JS to count up:

// Basic visitors counter
const visitors = new CountUp("visitors", 12457, { duration: 1.4 });
if (!visitors.error) visitors.start();

Formatted currency using built‑in options:

// Revenue with currency formatting
const revenue = new CountUp("revenue", 19999.95, {
  duration: 2.2,
  decimalPlaces: 2,
  separator: ",",
  decimal: ".",
  prefix: "$",
});
if (!revenue.error) revenue.start();

ESM import note:

// If your build supports ESM:
import { CountUp } from "countup.js";

const users = new CountUp("visitors", 12457, { duration: 1.4 });
users.start();

Tip: CountUp can target an element by id string or the element node itself.

Formatting and options

CountUp ships with sane defaults and helpful formatting switches. Common countup options include:

OptionPurposeExample
startValWhere to begin0
endValWhere to end12457
durationSeconds to animate1.6
decimalPlacesFixed decimals2
useEasingSmooth easing on/offtrue
separatorThousands separator”,“
decimalDecimal symbol”.“
prefixText before number”$“
suffixText after number”k”
numeralsCustom numeral glyphs[“٠”,“١”,…]
formattingFnFull control over outputvalue => customFormat(value)

Examples:

  • K‑style abbreviations
const el = document.querySelector("#installs");
const installs = new CountUp(el, 42_300, {
  duration: 1.2,
  suffix: " installs",
  separator: ",",
});
installs.start();
  • International currency (custom formatter)
const formatUSD = new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD",
});
const sales = new CountUp("sales", 50123.75, {
  duration: 2,
  decimalPlaces: 2,
  formattingFn: (n) => formatUSD.format(n),
});
sales.start();

See Intl.NumberFormat for robust locale‑aware currency and percentage formatting.

On-scroll triggers

Start animations only when visible to keep focus and save cycles. Here’s a straightforward IntersectionObserver countup pattern.

Single‑run on first view:

const els = document.querySelectorAll("[data-countup]");
const onceObserver = new IntersectionObserver(
  (entries, obs) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) return;
      const el = entry.target;
      const end = parseFloat(el.getAttribute("data-end") || "0");
      if (el.dataset.started) {
        obs.unobserve(el);
        return;
      }
      const cu = new CountUp(el, end, { duration: 1.6, separator: "," });
      if (!cu.error) {
        cu.start();
        el.dataset.started = "1"; // avoid double init
      }
      obs.unobserve(el);
    });
  },
  { threshold: 0.3 }
);

els.forEach((el) => onceObserver.observe(el));

Re‑trigger when the element re‑enters:

const map = new WeakMap();
const loopingObserver = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      const el = entry.target;
      if (entry.isIntersecting) {
        let cu = map.get(el);
        if (!cu) {
          const end = parseFloat(el.getAttribute("data-end") || "0");
          cu = new CountUp(el, end, { duration: 1.2, separator: "," });
          map.set(el, cu);
        } else {
          cu.reset();
        }
        cu.start();
      }
    });
  },
  { threshold: 0.3 }
);

document
  .querySelectorAll("[data-countup-loop]")
  .forEach((el) => loopingObserver.observe(el));

IntersectionObserver is widely supported. See the API reference on MDN.

Performance and a11y

A few tweaks go a long way:

  • Respect reduced motion: For users who prefer less animation, shorten or skip it.
const reduceMotion = window.matchMedia(
  "(prefers-reduced-motion: reduce)"
).matches;
const opts = { duration: reduceMotion ? 0 : 1.6, separator: "," };
new CountUp("visitors", 12457, opts).start();

Learn more about the media query on MDN.

  • Avoid layout shifts: Reserve space and use tabular figures so digits don’t jiggle.
.stat {
  display: inline-block;
  min-width: 6ch; /* reserve width for 5–6 digits */
  text-align: right;
  font-variant-numeric: tabular-nums; /* fixed-width numerals */
}
  • Announce meaning, not motion: Pair numbers with clear labels. If screen readers get noisy, keep the live region off by default and update only when necessary.

Troubleshooting

  • Double initialization: If a number starts multiple times, mark elements once (for example, with data-started) or store instances in a WeakMap (see examples above).

  • CDN vs ESM mismatch: If you import via ESM, use import { CountUp } from 'countup.js'. If you use a UMD build from a CDN, the global is CountUp. Don’t mix the two in the same scope.

  • SSR frameworks: If your build runs JavaScript on the server, wrap CountUp usage so it only runs in the browser (e.g., inside a client-only effect or a DOMContentLoaded handler) to avoid window is not defined errors.

  • NaN or wrong value: Sanitize your inputs. Strip non‑numeric characters before parsing, or store the target in a clean data attribute like data-end=“12345.67”.

  • Easing feels off: Try useEasing: false for a linear feel, or shorten duration for crisp updates.

FAQ

How do I format decimals cleanly?

CountUp has a decimalPlaces option for fixed decimals and decimal to control the symbol. For locale‑aware output, use a custom formattingFn with Intl.NumberFormat. Keep the end value numeric and let the formatter handle commas, periods, and grouping.

Can I format currency or percentages without a custom function?

Yes. For simple cases, use prefix (like $) or suffix (like %), plus decimalPlaces and separator. For complex locales or accounting formats, use formattingFn with Intl.NumberFormat for precise rules and rounding.

How do I run multiple counters on the same page?

Select all targets and loop. Keep duration modest and use one IntersectionObserver to watch them; it’s more efficient than many scroll listeners. If you need to re‑trigger, store each CountUp instance in a WeakMap keyed by the element.

What’s the difference between CountUp and Odometer?

CountUp focuses on precision and formatting—great for KPIs, totals, and dashboards. Odometer emphasizes a rolling reel effect that feels tactile. If you want the rolling style, see when to choose Odometer for rolling numbers; for most stats, CountUp’s clarity wins.

How do I start the count only when visible?

Use IntersectionObserver. Observe elements with a data attribute like data-end and start the counter the first time isIntersecting is true. Unobserve after start to avoid double runs, or reset and restart if you prefer re‑triggers.

Can CountUp start from an existing number in the DOM?

Yes. Read the text content, parse it to a number, and pass it as startVal. Alternatively, store both the start and end values in data attributes to keep the HTML clean and numeric.

Does CountUp work with jQuery sites?

It doesn’t require jQuery, but it works fine alongside it. If you want a jQuery‑centric plugin, see our guide to Counter‑Up. For modern builds, CountUp’s small footprint and options make it a solid default.

Any countup js examples for loading from a CDN?

Load the UMD build from a reputable CDN (e.g., jsDelivr) and use the global CountUp constructor in your inline or bundled scripts. Initialize on DOM ready, or pair it with IntersectionObserver for scroll‑based starts. Refer to the CountUp README at GitHub for current file names.