30% offEnding soon
useCountUpLoading saved progress…

useCountUp

useCountUp animates a number from a start value to an end value over a fixed duration, driven by requestAnimationFrame, and returns the current animated value on every frame. It is the hook behind the "stat counter" pattern — a dashboard number that rolls up from 0 to 1,284 when it scrolls into view instead of snapping into place. The detail that separates a correct implementation from a broken one is that the displayed value must be a function of how much time has passed, not of how many frames have rendered — so it finishes at the same wall-clock moment on a 30Hz, 60Hz, or 120Hz display.

Signature

useCountUp(
  end: number,
  options?: {
    start?: number;                 // value to animate from — default 0
    duration?: number;              // milliseconds — default 2000
    easing?: (t: number) => number; // maps progress 0..1 to an eased 0..1 — default linear
  },
): number; // the current animated value

Examples

// Animate 0 -> 200 over one second (linear).
const n = useCountUp(200, { duration: 1000 });
// elapsed 0ms    -> 0
// elapsed 500ms  -> 100   (half the time, half the distance)
// elapsed 1000ms -> 200   (lands exactly on the end)
// elapsed 1500ms -> 200   (stays put — the loop has stopped)
// A custom ease-out curve: quick start, gentle finish.
const easeOut = (t) => 1 - (1 - t) * (1 - t);
const n = useCountUp(100, { duration: 1000, easing: easeOut });
// elapsed 500ms -> 75      (0 + 100 * easeOut(0.5))

Notes

  • Time, not frames — derive progress from the frame's timestamp (elapsed / duration), never from a fixed per-frame increment. A += step where step assumes a 16ms frame silently drifts on any refresh rate that is not 60Hz.
  • Land exactly on end — when progress reaches 1, set the value to end and stop the loop. It must never overshoot the target.
  • Restart on change — when end changes, cancel the running animation and start a fresh one toward the new target.
  • Clean up — cancel the pending frame with cancelAnimationFrame on unmount so a dead component never calls setState.
  • Easing is injectable — accept an easing(t) that maps progress 0..1 to an eased 0..1; default to linear, t => t.
  • Out of scope — pause/resume controls, number formatting (commas, decimals), and "start when visible" are extensions, not the core loop.

FAQ

Why is useCountUp framerate-independent?
Because it computes the value from elapsed time (elapsed divided by duration) rather than a fixed per-frame increment, it reaches the end value at the same wall-clock moment on a 30Hz, 60Hz, or 120Hz display.
How does useCountUp avoid overshooting the target?
Once progress reaches 1 it sets the value to exactly the end value and stops scheduling frames, so the number never lands past its target.
What does useCountUp return?
It returns the current animated number on every frame. The signature is useCountUp(end, options), where options can set start, duration, and an easing function that maps progress from 0 to 1.