Skip to content
Docs
Counter

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