countUp.js
Animates a numerical value by counting up or down to a target, with configurable easing, number formatting, and scroll-triggered auto-animation.
Repository Health
Technical Analysis
countUp.js is a dependency-free JavaScript/TypeScript class that animates a number from a starting value to an end value, updating a DOM element (text, input, or SVG text/tspan) frame by frame via requestAnimationFrame. Despite its name it can count in either direction depending on the start and end values supplied.
It ships as an ES module with a UMD fallback and full TypeScript typings, and exposes a small imperative API (start, pauseResume, reset, update, onDestroy) plus a large set of formatting options — grouping separators, Indian numeral grouping, decimal places, custom numeral glyphs, prefixes/suffixes, and a pluggable render step for alternate animation styles like the Odometer plugin. The autoAnimate option wires up an IntersectionObserver so the count only starts once the target element scrolls into view, replacing the library’s older window-scroll-based scrollSpy implementation (kept only as a deprecated alias).
What You Get
- A single
CountUpclass with a small, well-documented options object covering duration, easing, decimal places, and separators - Built-in smart easing that automatically smooths the animation for very large jumps in value
- Scroll-triggered auto-animation via IntersectionObserver, with once-only and delay controls
- Support for plain elements,
<input>fields, and SVGtext/tspantargets - A plugin interface (
CountUpPlugin) for swapping in alternate render behavior, such as the community Odometer plugin - ES module and UMD builds plus TypeScript declaration files, so it drops into both bundler-based and plain-script projects
Common Use Cases
- Animating KPI or statistic counters on marketing and landing pages
- Live-updating dashboard metrics as new values arrive
- Counting up donation totals, follower counts, or other social-proof numbers
- Score or progress counters in games and interactive UIs
- Any numeric input or label that should visually count toward a new value instead of jumping instantly
Under The Hood
Architecture
countUp.js is a single self-contained CountUp class (src/countUp.ts) with no runtime dependencies. The constructor normalizes options against a defaults object, resolves the target element (by id or direct reference), and immediately paints the start value. Animation itself is a two-part state machine: determineDirectionAndSmartEasing decides direction and whether the total delta exceeds smartEasingThreshold, and if so splits the run into an eased first leg and a second corrective leg driven by update(); the per-frame count arrow-function callback (bound as a class field so it can be passed straight to requestAnimationFrame) computes frameVal, clamps overshoot, and calls printValue. Auto-animate wires an IntersectionObserver per element, tracked in a static WeakMap (observedElements) keyed by the DOM node so re-initializing a CountUp on the same element correctly unobserves the previous instance. Nothing in the class touches a framework lifecycle — onDestroy() is the only teardown hook, meant to be called explicitly by the consumer (or a framework wrapper) when the element unmounts.
Tech Stack
The library is written in TypeScript and compiled with tsc + Rollup (rollup-config.mjs, @rollup/plugin-terser for minification) into three artifacts: an ES module (dist/countUp.min.js), a UMD bundle (dist/countUp.umd.js), and hand-written .d.ts declarations. There is no runtime dependency at all — the only browser API surfaces used are requestAnimationFrame/cancelAnimationFrame and, for auto-animate, IntersectionObserver (with an explicit console error if the browser lacks it). A separate, optional requestAnimationFrame.polyfill.js is shipped in dist/ for older browsers. The package publishes both main (UMD) and module/exports.import (ES module) entry points so bundlers and plain <script type=module> usage both work.
Code Quality
Tests live in src/countUp.spec.ts and run under Jest with ts-jest and a jsdom environment; IntersectionObserver is hand-mocked in the spec file and requestAnimationFrame is stubbed with a deterministic frame-stepping function, letting the suite assert exact rendered string output frame-by-frame rather than relying on real timers. ESLint is configured via @typescript-eslint with the recommended rule sets (explicit-any and module-boundary-types rules turned off to keep the imperative DOM code pragmatic). There is no CI workflow file in the repository (only issue/PR templates and Dependabot config), so lint/test/build are run locally via npm scripts (npm run lint, npm t, npm run build) rather than gated automatically on push.
What Makes It Unique
The standout technical choice is the two-phase “smart easing” behavior: rather than applying one easing curve across an arbitrarily large numeric jump (which looks jarring — a rapid blur followed by a long, slow tail), the library detects when the delta exceeds a configurable threshold, disables easing for the bulk of the distance, then re-enables it only for a final, fixed-size corrective segment. Combined with the pluggable CountUpPlugin render interface — which lets a third-party package like the Odometer plugin replace the default text-formatting render step while reusing all of countUp’s timing and easing logic — this gives the library extensibility well beyond what its small API surface suggests, without adding any dependencies.