Docs

Counter

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

Installation

Update tailwind.config.js

theme: {
    extend: {
     colors: {
        background: "hsl(var(--background))",
     }
    },
  },

Install framer-motion

npm install framer-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 { useEffect, useRef } from "react";
import { useInView, useMotionValue, useSpring } from "framer-motion";
 
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,
  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) : value;
      }
    });
  }, [springValue, format]);
 
  return <span ref={ref} className={cn("text-4xl font-bold text-foreground", className)} />;
}

Credits

Built by hari