docsAnimated Number
Animated Number
An animated number component that smoothly transitions between values, featuring both slot-machine and score-style animations.
Slot Machine Style
Loading Preview...
Score Style
Loading Preview...
Install using CLI
npx shadcn@latest add "https://www.vengenceui.com/r/animated-number.json"Install Manually
1
Install dependencies
npm install framer-motion lucide-react clsx tailwind-merge
2
Add util file
lib/utils.ts
import { ClassValue, clsx } from "clsx";import { twMerge } from "tailwind-merge";export function cn(...inputs: ClassValue[]) {return twMerge(clsx(inputs));}
3
Copy the source code
Copy the code below and paste it into components/ui/animated-number.tsx
"use client";import React, { useEffect, useRef, useState } from "react";import { motion, AnimatePresence } from "framer-motion";import { cn } from "@/lib/utils";function AnimatedNumber({ value, className }: { value: number, className?: string }) {return (<div className={cn("flex items-center", className)}><div className="flex relative items-center">{value.toString().split("").map((digit, index) => (<SingleNumberHolder key={index} value={digit} index={index} />))}</div></div>)}function SingleNumberHolder({ value, index }: { value: string, index: number }) {const [height, setHeight] = useState<string | null>(null)const containerRef = useRef<HTMLDivElement>(null)let notANumber = falseuseEffect(() => {if (containerRef.current) {setHeight(getComputedStyle(containerRef.current).height)}}, [])if (index === 0) {notANumber = isNaN(Number.parseInt(value))}const vars = {init: { opacity: 0 },animate: { opacity: 1 },exit: { opacity: 0 },}return (<divclassName="relative"style={{ height: height || "auto", overflowY: "hidden", overflowX: "clip" }}ref={containerRef}>{notANumber && (<motion.spaninitial="init"animate="animate"exit="exit"variants={vars}key={value}layout="size">{value}</motion.span>)}{!notANumber && <RenderStrip value={value} eleHeight={height} />}</div>)}const zeroToNine = Array.from({ length: 10 }, (_, k) => k)function RenderStrip({ eleHeight, value }: { eleHeight: string | null, value: string }) {const heightInNumber = Number.parseInt(eleHeight?.replace("px", "") || "48")const negative = heightInNumber * -1const pos = heightInNumberconst prev = useRef(value)// Convert string values to numbers for comparisonconst currentVal = parseInt(value)const prevVal = parseInt(prev.current)// Calculate direction based on value changeconst diff = prevVal - currentValconst dir = currentVal > prevVal ? pos * diff * -1 : negative * diff// Update ref after calculationuseEffect(() => {prev.current = value}, [value])return (<AnimatePresence mode='wait'><motion.divkey={value}initial={{ y: dir }}animate={{ y: 0 }}exit={{ y: 0, transition: { duration: 0.1 } }}transition={{ duration: 0.5, ease: "easeOut" }}className='flex relative flex-col'>{/* Numbers smaller than current */}<motion.spanlayoutkey={`negative-${value}`}className={cn('flex flex-col items-center absolute bottom-full left-0')}>{zeroToNine.filter(val => val < currentVal).map((val, idx) => (<span key={`${val}_${idx}`}>{val}</span>))}</motion.span>{/* Current Number */}<span key={`current-${value}`}>{value}</span>{/* Numbers larger than current */}<motion.spanlayoutkey={`positive-${value}`}className={cn('flex flex-col items-center absolute top-full left-0')}>{zeroToNine.filter(val => val > currentVal).map((val, idx) => (<span key={`${val}_${idx}`}>{val}</span>))}</motion.span></motion.div></AnimatePresence>)}// Score-style animated number with color feedbackfunction AnimatedScore({ value, duration = 0.2, className }: { value: number, duration?: number, className?: string }) {const prevValueRef = useRef(value)useEffect(() => {prevValueRef.current = value}, [value])const colors = {negative: "#37ff1a",positive: "#ff1a4b",neutral: "#fff"}const transforVal = 80const forwards = {init: { y: transforVal * -1, opacity: 0, scale: 0.5, color: colors.negative },animate: {y: 0,opacity: 1,scale: [1.7, 1],color: [colors.negative, colors.negative, colors.neutral],transition: { duration: 0.4, times: [0, 0.7, 1], color: { times: [0, 0.75, 0.9] } },},exit: {y: transforVal,opacity: 0,scale: 0.5,color: colors.positive},}const backwards = {init: { y: transforVal, opacity: 0, scale: 0.5, color: colors.positive },animate: {y: 0,opacity: 1,scale: [1.7, 1],color: [colors.positive, colors.positive, colors.neutral],transition: { duration: 0.4, times: [0, 0.7, 1], color: { times: [0, 0.75, 0.9] } },},exit: {y: transforVal * -1,opacity: 0,scale: 0.5,color: colors.negative}}const variants = value >= prevValueRef.current ? forwards : backwardsconst direction = value >= prevValueRef.current ? "forwards" : "backwards"return (<div className={cn("relative flex justify-center items-center py-1 px-2 w-full rounded-md", className)}><motion.div layout="size" className='w-fit flex justify-center items-center'>{value.toString().split("").map((number, index) => (<ScoreContainerdirection={direction}duration={duration}variants={variants}number={number}key={index}/>))}</motion.div></div>)}function ScoreContainer({ number, variants, duration = 0.7, direction }: {number: string,variants: any,duration?: number,direction: string}) {const cached = React.useMemo(() => (<div className="relative"><AnimatePresence mode="popLayout"><motion.divanimate="animate"className="flex justify-center items-center"initial="init"exit="exit"variants={variants}key={number.toString()}layout="size"transition={{ duration, ease: "backInOut" }}>{number}</motion.div></AnimatePresence></div>), [number, direction, variants, duration])return <React.Fragment>{cached}</React.Fragment>}export { AnimatedNumber, AnimatedScore }
Usage
12"use client";3import { useState } from "react";4import { Minus, Plus } from "lucide-react";5import { motion } from "framer-motion";6import { AnimatedNumber, AnimatedScore } from "@/components/ui/animated-number"78export function UsageExample() {9return (10 <div className="flex flex-col gap-4">11 {/* Slot machine style */}12 <AnimatedScoreDemo />1314 {/* Score style with color feedback */}15 <AnimatedNumberDemo />16 </div>1718);19}2021function AnimatedNumberDemo() {22const [num, setNum] = useState(100);2324 function minus() {25 setNum(prev => prev - 10);26 }27 function add() {28 setNum(prev => prev + 10);29 }3031 return (32 <div className="flex bg-zinc-950 py-2.5 px-5 rounded-full items-center justify-center border border-zinc-800">33 <motion.button34 whileTap={{ scale: 0.9 }}35 onClick={minus}36 style={{ cursor: "pointer", backgroundColor: "#27272a " }}37 className="bg-zinc-800 text-zinc-400 hover:text-white h-11 w-11 flex justify-center items-center rounded-full transition-colors"38 >39 <Minus className="h-6 w-6" />40 </motion.button>41 <div className="flex relative px-6 text-5xl items-center font-mono text-white">42 <AnimatedNumber value={num} />43 </div>44 <motion.button45 whileTap={{ scale: 0.9 }}46 onClick={add}47 style={{ cursor: "pointer", backgroundColor: "#27272a " }}48 className="bg-zinc-800 text-zinc-400 hover:text-white h-11 w-11 flex justify-center items-center rounded-full transition-colors"49 >50 <Plus className="h-6 w-6" />51 </motion.button>52 </div>53 );5455}5657function AnimatedScoreDemo() {58const [score, setScore] = useState(100);5960 function decrease() {61 setScore(prev => prev - 10);62 }63 function increase() {64 setScore(prev => prev + 10);65 }6667 return (68 <div className="flex bg-zinc-950 py-2.5 px-5 rounded-full items-center justify-center border border-zinc-800">69 <motion.button70 whileTap={{ scale: 0.9 }}71 style={{ cursor: "pointer", backgroundColor: "#27272a " }}72 onClick={decrease}73 className="bg-zinc-800 text-zinc-400 hover:text-white h-11 w-11 flex justify-center items-center rounded-full transition-colors"74 >75 <Minus className="h-6 w-6" />76 </motion.button>77 <div className="flex relative px-6 text-5xl items-center font-mono">78 <AnimatedScore value={score} />79 </div>80 <motion.button81 whileTap={{ scale: 0.9 }}82 onClick={increase}83 style={{ cursor: "pointer", backgroundColor: "#27272a " }}84 className="bg-zinc-800 text-zinc-400 hover:text-white h-11 w-11 flex justify-center items-center rounded-full transition-colors"85 >86 <Plus className="h-6 w-6" />87 </motion.button>88 </div>89 );9091}9293
Props
AnimatedNumber
| Prop Name | Type | Default | Description |
|---|---|---|---|
| value | number | - | The numeric value to animate to. |
| className | string | - | Additional CSS classes for styling. |
AnimatedScore
| Prop Name | Type | Default | Description |
|---|---|---|---|
| value | number | - | The numeric value to animate to. |
| duration | number | 0.2 | Duration of the animation in seconds. |
| className | string | - | Additional CSS classes for styling. |