Skip to content

CountUp

Data Display

Animated numeric transitions with an immediate accessible target value.

Currency and decimalsChanging seat totalReplay by remountReduced-motion target
Open preview

Responsive viewport — resize the window to explore. Direction and density apply only to this preview. Theme applies to the docs too.

Usage

Use as a decorative enhancement beside a metric label. The server and initial render show the actual target value; animation starts after hydration. The first animation starts from from, and later target changes animate from the last displayed value. Remount to replay an initial count. Duration is bounded to ten seconds and decimal precision to six places. Prefix and suffix are text, not HTML. Formatting is en-US; non-finite values become zero.

Import
import { CountUp } from "@/components/ui/count-up";

Shipped as editable source in src/components/ui/count-up.tsx in both apps. Copy the implementation below when moving it to another project. Copy the complete example from Source code for all imports and state. Add "use client" when using hooks or event handlers. See installation and theming for project setup.

Component source

The complete implementation is included in both apps. It uses the React, Radix, Lucide, and shared-core dependencies already installed in Apex. Keep your project’s semantic theme tokens when reusing it.

src/components/ui/count-up.tsx
"use client";
import { useEffect, useRef, useState } from "react";
export interface CountUpProps {
  value: number;
  from?: number;
  duration?: number;
  decimals?: number;
  prefix?: string;
  suffix?: string;
  className?: string;
}
export function CountUp({
  value,
  from = 0,
  duration = 1000,
  decimals = 0,
  prefix = "",
  suffix = "",
  className,
}: CountUpProps) {
  const target = Number.isFinite(value) ? value : 0;
  const [display, setDisplay] = useState(target);
  const current = useRef<number | null>(null);
  const precision = Math.max(0, Math.min(6, Math.floor(decimals) || 0));
  useEffect(() => {
    const start = current.current ?? (Number.isFinite(from) ? from : 0);
    const length = Math.max(
      0,
      Math.min(10000, Number.isFinite(duration) ? duration : 1000),
    );
    const media = matchMedia("(prefers-reduced-motion: reduce)");
    let frame = 0;
    let started: number | undefined;
    let cancelled = false;
    const tick = (time: number) => {
      if (cancelled) return;
      started ??= time;
      const progress =
        media.matches || length === 0
          ? 1
          : Math.min(1, (time - started) / length);
      const next = start + (target - start) * (1 - Math.pow(1 - progress, 3));
      current.current = next;
      setDisplay(next);
      if (progress < 1) frame = requestAnimationFrame(tick);
    };
    const finish = () => {
      cancelAnimationFrame(frame);
      frame = requestAnimationFrame(() => {
        current.current = target;
        setDisplay(target);
      });
    };
    frame = requestAnimationFrame(tick);
    media.addEventListener("change", finish);
    return () => {
      cancelled = true;
      cancelAnimationFrame(frame);
      media.removeEventListener("change", finish);
    };
  }, [target, from, duration]);
  const format = (number: number) =>
    `${prefix}${new Intl.NumberFormat("en-US", { minimumFractionDigits: precision, maximumFractionDigits: precision }).format(number)}${suffix}`;
  return (
    <span className={className}>
      <span aria-hidden>{format(display)}</span>
      <span className="sr-only">{format(target)}</span>
    </span>
  );
}

API reference

Common props and defaults. Slot-specific props are named below; native elements also accept their standard HTML attributes. The component TypeScript definitions are the complete type reference.

CountUp props
PropType / DefaultDescription
valuenumber

Default: required

Target value.
fromnumber

Default: 0

Initial animation start.
durationnumber

Default: 1000

Animation duration in milliseconds, clamped to 0–10000.
decimalsnumber

Default: 0

Formatted precision, clamped to 0–6.
prefix / suffixstring

Default: empty

Text surrounding the formatted value.
classNamestring

Default: —

Extend the container with semantic theme utilities.

Build with these too