Animated Number

An animated number component that animates the transition between two numbers.

Example

Countdown

0123456789
0123456789
:
0123456789
0123456789
:
0123456789
0123456789

Installation

Copy and paste the source code into your project.

"use client";
 
import { useEffect } from "react";
import { motion, useSpring, useTransform, MotionValue } from "framer-motion";
import { cn } from "@/lib/utils";
 
type Sizing = "sm" | "md" | "lg";
 
function calculateDimensions(size: Sizing) {
  switch (size) {
    case "sm":
      return { fontSize: 8, padding: 2 };
    case "md":
      return { fontSize: 12, padding: 2 };
    case "lg":
      return { fontSize: 16, padding: 1 };
    default:
      return { fontSize: 12, padding: 2 };
  }
}
 
function Number({
  mv,
  number,
  height,
}: {
  mv: MotionValue;
  number: number;
  height: number;
}) {
  let y = useTransform(mv, (latest) => {
    let offset = (10 + number - latest) % 10;
 
    let memo = offset * height;
 
    if (offset > 5) {
      memo -= 10 * height;
    }
 
    return memo;
  });
 
  return (
    <motion.span
      style={{ y }}
      className="absolute inset-0 flex items-center justify-center"
    >
      {number}
    </motion.span>
  );
}
 
type Props = {
  place: number;
  value: number;
  size?: Sizing;
};
 
/**
 * AnimatedNumber component.
 *
 * An animated number component that animates the transition between two numbers.
 *
 * @param {number} [props.place] - The place value of the digit (10s, 1s, etc.)
 * @param {number} [props.value] - The value of the digit (0-9)
 * @param {Sizing} [props.size] - The size of the number ('sm', 'md', 'lg')
 */
export function AnimatedNumber({ place, value, size = "lg" }: Props) {
  const { fontSize, padding } = calculateDimensions(size);
  const height = fontSize + padding;
  const width = fontSize + padding;
 
  const valueRoundedToPlace = Math.floor(value / place) % 10;
 
  const animatedValue = useSpring(valueRoundedToPlace, {
    stiffness: 200,
    damping: 20,
  });
 
  useEffect(() => {
    animatedValue.set(valueRoundedToPlace);
  }, [animatedValue, valueRoundedToPlace]);
 
  return (
    <motion.div
      className={cn(`relative overflow-hidden`)}
      style={{ width: `${width}px`, height: `${height}px` }}
    >
      {Array.from(Array(10).keys()).map((i) => (
        <Number key={i} mv={animatedValue} number={i} height={height} />
      ))}
    </motion.div>
  );
}

    Update the import paths to match your project setup.

    More Examples

    Counter

    0123456789
    0123456789
    0123456789
    0123456789

    Scramble Counter

    0123456789
    0123456789
    0123456789
    0123456789

    Props

    PropTypeDefaultDescription
    placenumber-The place value of the digit (10s, 1s, etc.)
    valuenumber-The value of the digit (0-9)
    sizeSizing"sm"The size of the number ('sm', 'md', 'lg')