Number Ticker
Springs a number up when it enters the viewport.
0.8 KB gzippedDeps: motion/reactReduced-motion safeBase UI & Radix compatible
0+
Installation
npx shadcn@latest add https://velora.colorlib.com/r/number-ticker.jsonInstalls with: motion
Source
"use client";
import { useEffect, useRef } from "react";
import {
useInView,
useMotionValue,
useReducedMotion,
useSpring,
} from "motion/react";
import { cn } from "@/lib/utils";
interface NumberTickerProps extends React.HTMLAttributes<HTMLSpanElement> {
value: number;
startValue?: number;
/** Seconds to wait after entering the viewport */
delay?: number;
decimalPlaces?: number;
prefix?: string;
suffix?: string;
}
export function NumberTicker({
value,
startValue = 0,
delay = 0,
decimalPlaces = 0,
prefix = "",
suffix = "",
className,
...props
}: NumberTickerProps) {
const ref = useRef<HTMLSpanElement>(null);
const motionValue = useMotionValue(startValue);
const springValue = useSpring(motionValue, {
damping: 60,
stiffness: 100,
});
const isInView = useInView(ref, { once: true, margin: "0px 0px -10% 0px" });
const reducedMotion = useReducedMotion();
const format = (n: number) =>
`${prefix}${Intl.NumberFormat("en-US", {
minimumFractionDigits: decimalPlaces,
maximumFractionDigits: decimalPlaces,
}).format(n)}${suffix}`;
useEffect(() => {
if (!isInView) return;
if (reducedMotion) {
motionValue.jump(value);
return;
}
const timeout = setTimeout(() => motionValue.set(value), delay * 1000);
return () => clearTimeout(timeout);
}, [isInView, reducedMotion, motionValue, value, delay]);
useEffect(
() =>
springValue.on("change", (latest) => {
if (ref.current) ref.current.textContent = format(latest);
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[springValue, prefix, suffix, decimalPlaces]
);
return (
<span
ref={ref}
data-slot="number-ticker"
className={cn("inline-block tabular-nums", className)}
{...props}
>
{format(startValue)}
</span>
);
}