LogoLaviecn

Counting Number

Demo

Installation

Shadcn CLI

npx shadcn@latest add https://laviecn.vercel.app/r/counting-number.json
pnpm dlx shadcn@latest add https://laviecn.vercel.app/r/counting-number.json
yarn dlx shadcn@latest add https://laviecn.vercel.app/r/counting-number.json
bun x shadcn@latest add https://laviecn.vercel.app/r/counting-number.json

Manual

Install the following dependencies.

npm install motion
pnpm add motion
yarn add motion
bun add motion

Copy and paste the following code into your project.

counting-number.tsx
"use client"

import { useEffect, useRef } from "react"
import { useInView, useMotionValue, useSpring } from "motion/react"

interface CountingNumber {
  to: number
  from?: number
  direction?: "up" | "down"
  delay?: number
  duration?: number
  className?: string
  startWhen?: boolean
  separator?: string
  onStart?: () => void
  onEnd?: () => void
}

export function CountingNumber({
  to,
  from = 0,
  direction = "up",
  delay = 0,
  duration = 2,
  className = "",
  startWhen = true,
  separator = "",
  onStart,
  onEnd,
}: CountingNumber) {
  const ref = useRef<HTMLSpanElement>(null)
  const motionValue = useMotionValue(direction === "down" ? to : from)

  const damping = 20 + 40 * (1 / duration)
  const stiffness = 100 * (1 / duration)

  const springValue = useSpring(motionValue, {
    damping,
    stiffness,
  })

  const isInView = useInView(ref, { once: true, margin: "0px" })

  useEffect(() => {
    if (ref.current) {
      ref.current.textContent = String(direction === "down" ? to : from)
    }
  }, [from, to, direction])

  useEffect(() => {
    if (isInView && startWhen) {
      if (typeof onStart === "function") {
        onStart()
      }

      const timeoutId = setTimeout(() => {
        motionValue.set(direction === "down" ? from : to)
      }, delay * 1000)

      const durationTimeoutId = setTimeout(
        () => {
          if (typeof onEnd === "function") {
            onEnd()
          }
        },
        delay * 1000 + duration * 1000
      )

      return () => {
        clearTimeout(timeoutId)
        clearTimeout(durationTimeoutId)
      }
    }
  }, [
    isInView,
    startWhen,
    motionValue,
    direction,
    from,
    to,
    delay,
    onStart,
    onEnd,
    duration,
  ])

  useEffect(() => {
    const unsubscribe = springValue.on("change", (latest) => {
      if (ref.current) {
        const options = {
          useGrouping: !!separator,
          minimumFractionDigits: 0,
          maximumFractionDigits: 0,
        }

        const formattedNumber = Intl.NumberFormat("en-US", options).format(
          Number(latest.toFixed(0))
        )

        ref.current.textContent = separator
          ? formattedNumber.replace(/,/g, separator)
          : formattedNumber
      }
    })

    return () => unsubscribe()
  }, [springValue, separator])

  return <span className={`${className}`} ref={ref} />
}

Update the import paths to match your project setup.

Props

CountUpProps

PropTypeDefault
to
number
-
from?
number
0
direction?
string
up
delay?
number
0
duration?
number
2
className?
string
-
startWhen?
boolean
true
separator?
string
-
onStart?
() => void
-
onEnd?
() => void
-