Sliding Number Counter
Sliding Number Counter is a React and Tailwind CSS component from Motion Primitives, licensed MIT. Copy it and paste it into your project.
Motion Primitives
MIT
other
What it is
Sliding Number Counter is a React and Tailwind CSS component from Motion Primitives, 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 Motion Primitives and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Sliding Number Counter
'use client';
import { useEffect, useId } from 'react';
import {
MotionValue,
motion,
useSpring,
useTransform,
motionValue,
} from 'motion/react';
import useMeasure from 'react-use-measure';
const TRANSITION = {
type: 'spring' as const,
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>
);
}