NEW
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
Learn more
Skip to content
Docs

Counter

A counter component that counts up to given number. Works with both increment and decrement.

Loading...

Installation

CLI

pnpm dlx shadcn@latest add https://animata.design/r/text/counter.json

Manual

Install motion

npm install motion

Run the following command

It will create a new file called counter.tsx inside the components/animata/text directory.

mkdir -p components/animata/text && touch components/animata/text/counter.tsx

Paste the code

Open the newly created file and paste the following code:

import { useInView, useMotionValue, useSpring } from "motion/react";
import { useEffect, useRef } from "react";
 
import { cn } from "@/lib/utils";
 
interface CounterProps {
  /**
   * A function to format the counter value. By default, it will format the
   * number with commas.
   */
  format?: (value: number) => string;
 
  /**
   * The target value of the counter.
   */
  targetValue: number;
 
  /**
   * The direction of the counter. If "up", the counter will start from 0 and
   * go up to the target value. If "down", the counter will start from the target
   * value and go down to 0.
   */
  direction?: "up" | "down";
 
  /**
   * The delay in milliseconds before the counter starts counting.
   */
  delay?: number;
 
  /**
   * Additional classes for the counter.
   */
  className?: string;
}
 
export const Formatter = {
  number: (value: number) => Intl.NumberFormat("en-US").format(+value.toFixed(0)),
  currency: (value: number) =>
    Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(+value.toFixed(0)),
};
 
export default function Counter({
  format = Formatter.number,
  targetValue = 1000,
  direction = "up",
  delay = 0,
  className,
}: CounterProps) {
  const ref = useRef<HTMLSpanElement>(null);
  const isGoingUp = direction === "up";
  const motionValue = useMotionValue(isGoingUp ? 0 : targetValue);
 
  const springValue = useSpring(motionValue, {
    damping: 60,
    stiffness: 80,
  });
  const isInView = useInView(ref, { margin: "0px", once: true });
 
  useEffect(() => {
    if (!isInView) {
      return;
    }
 
    const timer = setTimeout(() => {
      motionValue.set(isGoingUp ? targetValue : 0);
    }, delay);
 
    return () => clearTimeout(timer);
  }, [isInView, delay, isGoingUp, targetValue, motionValue]);
 
  useEffect(() => {
    springValue.on("change", (value) => {
      if (ref.current) {
        ref.current.textContent = format ? format(value) : String(value);
      }
    });
  }, [springValue, format]);
 
  const initialDisplay = format
    ? format(isGoingUp ? 0 : targetValue)
    : String(isGoingUp ? 0 : targetValue);
 
  return (
    <span ref={ref} className={cn("text-4xl font-bold text-foreground", className)}>
      {initialDisplay}
    </span>
  );
}

Credits

Built by hari