A React number component with scrolling digits that roll to the new value. Built with Motion for counters, stats and prices.
<RollingNumber
className="font-semibold font-serif text-6xl"
>
{value}
</RollingNumber>npm install motionbun add motiondeno add motionpnpm add motionyarn add motionCopy and paste the following code into your project.
"use client";
import { AnimatePresence, motion } from "motion/react";
import type * as React from "react";
import { cn } from "@/lib/utils";
const slotTransition = {
clipPath: { duration: 0.4, ease: [0.34, 1.56, 0.64, 1] as const },
width: { duration: 0.35, ease: [0.22, 1, 0.36, 1] as const },
};
function RollingDigit({ digit, delay = 0 }: { digit: number; delay?: number }) {
return (
<span className="relative inline-block h-lh overflow-hidden leading-none">
<motion.span
animate={{ y: `${-digit * 10}%` }}
className="flex flex-col"
transition={{ bounce: 0.12, delay, duration: 0.5, type: "spring" }}
>
{Array.from({ length: 10 }, (_, i) => (
<span className="block h-lh leading-none" key={i}>
{i}
</span>
))}
</motion.span>
</span>
);
}
type RollingNumberProps<T extends React.ElementType = "span"> = {
as?: T;
children: number | string;
className?: string;
staggerDelayMs?: number;
} & Omit<
React.ComponentPropsWithoutRef<T>,
"as" | "children" | "className" | "staggerDelayMs"
>;
function RollingNumber<T extends React.ElementType = "span">({
as,
children,
className,
staggerDelayMs = 0,
...props
}: RollingNumberProps<T>) {
const Tag = (as ?? "span") as React.ElementType;
const raw = String(children);
const isNegative = raw.startsWith("-");
const absolute = isNegative ? raw.slice(1) : raw;
const [intPart, decPart] = absolute.split(".");
const intDigits = intPart!.split("");
const decDigits = decPart ? decPart.split("") : [];
const hasDecimal = decPart !== undefined;
const staggerDelay = (index: number) => (index * staggerDelayMs) / 1000;
return (
<Tag
className={cn("inline-flex items-center tabular-nums", className)}
{...props}
>
{isNegative && <span>-</span>}
{/* Integer digits — keyed from the right so leading digits appear/disappear */}
<AnimatePresence initial={false}>
{intDigits.map((digit, index) => (
<motion.span
animate={{ clipPath: "inset(0 0 0% 0)", width: "auto" }}
className="inline-block overflow-hidden"
exit={{ clipPath: "inset(0 0 100% 0)", width: 0 }}
initial={{ clipPath: "inset(0 0 100% 0)", width: 0 }}
key={intDigits.length - 1 - index}
transition={{ ...slotTransition, delay: staggerDelay(index) }}
>
<RollingDigit delay={staggerDelay(index)} digit={Number(digit)} />
</motion.span>
))}
</AnimatePresence>
{/* Decimal dot — no rolling, just width */}
<AnimatePresence initial={false}>
{hasDecimal && (
<motion.span
animate={{ width: "auto" }}
className="inline-block overflow-hidden"
exit={{ width: 0 }}
initial={{ width: 0 }}
key="dot"
transition={{
delay: staggerDelay(intDigits.length),
duration: 0.35,
ease: [0.22, 1, 0.36, 1],
}}
>
.
</motion.span>
)}
</AnimatePresence>
{/* Decimal digits — keyed from the left (anchored to decimal point) */}
<AnimatePresence initial={false}>
{decDigits.map((digit, index) => {
const flatIndex = intDigits.length + 1 + index;
return (
<motion.span
animate={{ clipPath: "inset(0 0 0% 0)", width: "auto" }}
className="inline-block overflow-hidden"
exit={{ clipPath: "inset(0 0 100% 0)", width: 0 }}
initial={{ clipPath: "inset(0 0 100% 0)", width: 0 }}
key={`dec-${index}`}
transition={{ ...slotTransition, delay: staggerDelay(flatIndex) }}
>
<RollingDigit
delay={staggerDelay(flatIndex)}
digit={Number(digit)}
/>
</motion.span>
);
})}
</AnimatePresence>
</Tag>
);
}
export { RollingNumber };<BounceNumber
value={value}
className="text-6xl font-semibold"
aria-label={value.toString()}
/><BounceNumber
value={value}
className="text-6xl font-semibold"
aria-label={value.toString()}
staggerDelaySeconds={0.05} // default is 0.03
/><BounceNumber
value={value}
className="text-6xl font-semibold"
aria-label={value.toString()}
animateOnMount={true} // default is false
/>It extends the props of the HTML tag it renders as. It also accepts the following custom props:
| Prop | Type | Default Value | Description |
|---|---|---|---|
| value | number | - | The value to display. |
| staggerDelaySeconds | number | 0.03 | The delay between each digit. |
| animateOnMount | boolean | false | Whether to animate the component when it is mounted. |
| as | React.ElementType | 'div' | The HTML tag to render the component as. |