A counter component that counts up to given number. Works with both increment and decrement.
tailwind.config.js
theme: { extend: { colors: { background: "hsl(var(--background))", } }, },
framer-motion
npm install framer-motion
It will create a new file called counter.tsx inside the components/animata/text directory.
counter.tsx
components/animata/text
mkdir -p components/animata/text && touch components/animata/text/counter.tsx
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)} />; }
Built by hari