VibeUI
ComponentsDocs
VibeUI

Components with taste, that you own. Copy the source, retint the tokens, keep every line.

Library

  • All components
  • Signature pieces
  • Text effects
  • Footers

Docs

  • Introduction
  • Installation
  • Theming
  • CLI

Project

  • GitHub
  • Agents and MCP
  • Issues
  • MIT license
© 2026 VibeUI · MIT licensed
  1. Components
  2. Signature
  3. Count Up

Count Up

Number that eases up from zero the first time it scrolls into view.

SignatureClient component
0

components

0.0%

pure CSS motion

Install

bash
npx @agents-npm-packages/vibeui add count-up --dir ./src/components/ui

Import

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

API

Generated from the component's TypeScript types, so it always matches the source. * marks a required prop.

<CountUp>

PropTypeDefaultDescription
value*number—Controlled value.
durationnumber1400Animation length in milliseconds (or seconds where noted).
decimalsnumber0Fraction digits to display.
suffixstring""Text appended to the number.
prefixstring""Text prepended to the number.
classNamestring—Extra classes, merged with the component's own.

Source

src/components/ui/count-up.tsx
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";

/** Number that eases up from zero the first time it scrolls into view. */
export function CountUp({
  value,
  duration = 1400,
  decimals = 0,
  suffix = "",
  prefix = "",
  className,
}: {
  value: number;
  duration?: number;
  decimals?: number;
  suffix?: string;
  prefix?: string;
  className?: string;
}) {
  const ref = React.useRef<HTMLSpanElement>(null);
  const [display, setDisplay] = React.useState(0);
  const [started, setStarted] = React.useState(false);

  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const io = new IntersectionObserver(
      ([entry]) => {
        if (entry?.isIntersecting) {
          setStarted(true);
          io.disconnect();
        }
      },
      { threshold: 0.4 },
    );
    io.observe(el);
    return () => io.disconnect();
  }, []);

  React.useEffect(() => {
    if (!started) return;
    const start = performance.now();
    let raf = 0;
    const tick = (now: number) => {
      const t = Math.min(1, (now - start) / duration);
      setDisplay(value * (1 - Math.pow(1 - t, 4)));
      if (t < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [started, value, duration]);

  return (
    <span
      ref={ref}
      aria-label={`${prefix}${value}${suffix}`}
      className={cn("font-medium tabular-nums tracking-[-0.04em] text-ink", className)}
    >
      {prefix}
      {display.toLocaleString("en-US", {
        minimumFractionDigits: decimals,
        maximumFractionDigits: decimals,
      })}
      {suffix}
    </span>
  );
}

Exports CountUp. Peer dependencies: clsx, tailwind-merge.

PreviousConfettiNextCover Flow