Sliding Number
A component that slides numbers.
0
0123456789"use client";
import { motion } from "motion/react";
import { useEffect, useState } from "react";
import { SlidingNumber } from "@/components/core/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>
);
}
Installation
Install the following dependencies:
npm install react-use-measure motion
Copy and paste the following code into your project:
"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",
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>
);
}
Update the import paths to match your project setup.
Usage
Basic
"use client";
import { motion } from "motion/react";
import { useEffect, useState } from "react";
import { SlidingNumber } from "@/components/core/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>
);
}
Clock
0
01234567890
01234567890
01234567890
01234567890
01234567890
0123456789"use client";
import { SlidingNumber } from "@/components/core/sliding-number";
import { useEffect, useState } from "react";
export default function Clock() {
const [hours, setHours] = useState(new Date().getHours());
const [minutes, setMinutes] = useState(new Date().getMinutes());
const [seconds, setSeconds] = useState(new Date().getSeconds());
useEffect(() => {
const interval = setInterval(() => {
setHours(new Date().getHours());
setMinutes(new Date().getMinutes());
setSeconds(new Date().getSeconds());
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<div className="flex items-center gap-0.5 text-xl">
<SlidingNumber value={hours} padStart={true} />
<span>:</span>
<SlidingNumber value={minutes} padStart={true} />
<span>:</span>
<SlidingNumber value={seconds} padStart={true} />
</div>
);
}
Props
Prop | Type | Default |
---|---|---|
decimalSeparator? | string | "." |
padStart? | boolean | false |
value? | number | - |
Scroll Progress Bar
The scroll progress bar adds a dynamic, interactive scrolling indicator, built with Motion and Tailwind CSS, enhancing user experience with smooth transitions.
Text Gradient Scroll
Discover the allure of animated gradient text, a dynamic UI component that enhances user engagement with smooth color transitions. Built with Tailwind CSS and Motion.