Sliding Number Counter
Sliding Number Counter is a React and Tailwind CSS component from Bund UI, licensed MIT. Copy it and paste it into your project.
Bund UI
MIT
other
What it is
Sliding Number Counter is a React and Tailwind CSS component from Bund UI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i motion react-use-measure
Licence
This component comes from Bund UI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Sliding Number Counter
"use client";
import { motion } from "motion/react";
import { useEffect, useState } from "react";
import { SlidingNumber } from "./sliding-number";
export default function SlidingNumberBasic() {
const [value, setValue] = useState(0);
useEffect(() => {
if (value === 100) return;
const interval = setInterval(() => {
setValue(value + 1);
}, 10);
return () => clearInterval(interval);
}, [value]);
return (
<motion.div
initial={{ y: 0, fontSize: `${24}px` }}
animate={{ y: 0, fontSize: `${24}px` }}
transition={{
ease: [1, 0, 0.35, 0.95],
duration: 1.5,
delay: 0.3
}}>
<div className="inline-flex items-center">
<SlidingNumber value={value} />%
</div>
</motion.div>
);
}
"use client";
import { useEffect, useId } from "react";
import { MotionValue, motion, useSpring, useTransform, motionValue } from "motion/react";
import { Transition } from "motion";
import useMeasure from "react-use-measure";
const TRANSITION: Transition = {
type: "spring",
stiffness: 280,
damping: 18,
mass: 0.3
};
function Digit({ value, place }: { value: number; place: number }) {
const valueRoundedToPlace = Math.floor(value / place) % 10;
const initial = motionValue(valueRoundedToPlace);
const animatedValue = useSpring(initial, TRANSITION);
useEffect(() => {
animatedValue.set(valueRoundedToPlace);
}, [animatedValue, valueRoundedToPlace]);
return (
<div className="relative inline-block w-[1ch] overflow-x-visible overflow-y-clip leading-none tabular-nums">
<div className="invisible">0</div>
{Array.from({ length: 10 }, (_, i) => (
<Number key={i} mv={animatedValue} number={i} />
))}
</div>
);
}
function Number({ mv, number }: { mv: MotionValue<number>; number: number }) {
const uniqueId = useId();
const [ref, bounds] = useMeasure();
const y = useTransform(mv, (latest) => {
if (!bounds.height) return 0;
const placeValue = latest % 10;
const offset = (10 + number - placeValue) % 10;
let memo = offset * bounds.height;
if (offset > 5) {
memo -= 10 * bounds.height;
}
return memo;
});
// don't render the animated number until we know the height
if (!bounds.height) {
return (
<span ref={ref} className="invisible absolute">
{number}
</span>
);
}
return (
<motion.span
style={{ y }}
layoutId={`${uniqueId}-${number}`}
className="absolute inset-0 flex items-center justify-center"
transition={TRANSITION}
ref={ref}>
{number}
</motion.span>
);
}
type SlidingNumberProps = {
value: number;
padStart?: boolean;
decimalSeparator?: string;
};
export function SlidingNumber({
value,
padStart = false,
decimalSeparator = "."
}: SlidingNumberProps) {
const absValue = Math.abs(value);
const [integerPart, decimalPart] = absValue.toString().split(".");
const integerValue = parseInt(integerPart, 10);
const paddedInteger = padStart && integerValue < 10 ? `0${integerPart}` : integerPart;
const integerDigits = paddedInteger.split("");
const integerPlaces = integerDigits.map((_, i) => Math.pow(10, integerDigits.length - i - 1));
return (
<div className="flex items-center">
{value < 0 && "-"}
{integerDigits.map((_, index) => (
<Digit
key={`pos-${integerPlaces[index]}`}
value={integerValue}
place={integerPlaces[index]}
/>
))}
{decimalPart && (
<>
<span>{decimalSeparator}</span>
{decimalPart.split("").map((_, index) => (
<Digit
key={`decimal-${index}`}
value={parseInt(decimalPart, 10)}
place={Math.pow(10, decimalPart.length - index - 1)}
/>
))}
</>
)}
</div>
);
}