Animated Number
Animated number with different effects
1
Animated Number
A text reveal component that reveals text with different effects.
Installation
Use the following command to install the component:
npx shadcn@latest add https://ui.sanjid.shop/r/animated-number.jsonpnpm dlx shadcn@latest add https://ui.sanjid.shop/r/animated-number.jsonyarn shadcn@latest add https://ui.sanjid.shop/r/animated-number.jsonbunx --bun shadcn@latest add https://ui.sanjid.shop/r/animated-number.json
Install the following dependencies:
npm install motionpnpm add motionyarn add motionbun add motion
Add util file
lib/utils.ts
1import { ClassValue, clsx } from "clsx";2import { twMerge } from "tailwind-merge";3 4export function cn(...inputs: ClassValue[]) {5 return twMerge(clsx(inputs));6}
Copy and paste the following code into your project.
1"use client";2 3import { cn } from "@/lib/utils";4import { motion } from "motion/react";5import React, { useMemo } from "react";6 7// ---------------------------------------------------------8// SIZE MAP — flexible for text scaling9// ---------------------------------------------------------10const sizeMap = {11 sm: { fontSize: "1.5rem", height: "1.5rem", width: "1.75ch" },12 md: { fontSize: "2rem", height: "2rem", width: "2ch" },13 lg: { fontSize: "3rem", height: "3rem", width: "2.75ch" },14 xl: { fontSize: "4rem", height: "4rem", width: "3.5ch" },15 "2xl": { fontSize: "5rem", height: "5rem", width: "4.5ch" },16} as const;17 18export type AnimatedSize = keyof typeof sizeMap;19 20interface AnimatedNumberProps {21 number: number | string;22 prefix?: string;23 suffix?: string;24 label?: string;25 size?: AnimatedSize;26 className?: string;27 digitClassName?: string;28 durationPerDigit?: number;29 blur?: boolean;30 direction?: "up" | "down"; // ⬆️⬇️ new feature31 easing?: [number, number, number, number]; // customizable motion easing32}33 34// ---------------------------------------------------------35// AnimatedNumber Component36// ---------------------------------------------------------37export const AnimatedNumber: React.FC<AnimatedNumberProps> = ({38 number,39 prefix = "",40 suffix = "",41 label,42 size = "xl",43 className,44 digitClassName,45 durationPerDigit = 0.6,46 blur = false,47 direction = "up",48 easing = [0.22, 1, 0.36, 1],49}) => {50 const digits = useMemo(() => number.toString().split(""), [number]);51 const sizeStyle = sizeMap[size];52 53 return (54 <div className={cn("flex flex-col items-start gap-1", className)}>55 <div className="flex items-end">56 {prefix && <StaticChar char={prefix} sizeStyle={sizeStyle} />}57 58 {digits.map((char, idx) =>59 /\d/.test(char) ? (60 <AnimatedDigit61 key={`${idx}-${number}`}62 target={parseInt(char)}63 delay={idx * 60}64 size={size}65 durationPerDigit={durationPerDigit}66 blur={blur}67 direction={direction}68 easing={easing}69 className={digitClassName}70 />71 ) : (72 <StaticChar key={idx} char={char} sizeStyle={sizeStyle} />73 ),74 )}75 76 {suffix && <StaticChar char={suffix} sizeStyle={sizeStyle} />}77 </div>78 79 {label && (80 <span className="text-muted-foreground text-sm tracking-wide">81 {label}82 </span>83 )}84 </div>85 );86};87 88// ---------------------------------------------------------89// Static Char (for prefix, suffix, separators)90// ---------------------------------------------------------91const StaticChar: React.FC<{92 char: string;93 sizeStyle: { fontSize: string; height: string };94}> = ({ char, sizeStyle }) => (95 <span96 className="font-light tabular-nums"97 style={{98 fontSize: sizeStyle.fontSize,99 lineHeight: sizeStyle.height,100 }}101 >102 {char}103 </span>104);105 106// ---------------------------------------------------------107// AnimatedDigit Component108// ---------------------------------------------------------109interface AnimatedDigitProps {110 target: number;111 delay?: number;112 size?: AnimatedSize;113 blur?: boolean;114 durationPerDigit?: number;115 direction?: "up" | "down";116 easing?: [number, number, number, number];117 className?: string;118}119 120export const AnimatedDigit: React.FC<AnimatedDigitProps> = ({121 target,122 delay = 0,123 size = "xl",124 blur = false,125 durationPerDigit = 0.6,126 direction = "up",127 easing = [0.22, 1, 0.36, 1],128 className,129}) => {130 const { height, fontSize, width } = sizeMap[size];131 const heightValue = parseFloat(height.replace("rem", ""));132 133 const digits = useMemo(() => Array.from({ length: 10 }, (_, i) => i), []);134 135 return (136 <div137 className={cn(138 "inline-block overflow-hidden text-center tabular-nums",139 blur && "bg-white/10 backdrop-blur-xs dark:bg-black/10",140 )}141 style={{ height, width, lineHeight: height }}142 >143 <motion.div144 initial={{ y: 0 }}145 animate={{146 y:147 direction === "up"148 ? `-${target * heightValue}rem`149 : `${target * heightValue}rem`,150 }}151 transition={{152 delay: delay / 1000,153 duration: durationPerDigit + target * 0.04,154 ease: easing,155 }}156 >157 {digits.map((digit) => (158 <div159 key={digit}160 className={cn("font-light select-none", className)}161 style={{ height, fontSize }}162 >163 {digit}164 </div>165 ))}166 </motion.div>167 </div>168 );169};
Update the import paths to match your project setup.
Basic Usage
1<AnimatedNumber number={1234567890} />
Advanced Usage
1<AnimatedNumber number={1234567890} />
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| number | number | No | – | The number to animate |
Best Practices
- Clear Labels: Use descriptive and concise text reveal labels
- Consistent Icons: Use consistent icon styles throughout
- Logical Grouping: Group related text reveal items together
- Keyboard Navigation: Ensure all items are accessible via keyboard
- Loading States: Show loading indicators for dynamic content