AnimatedMotion
Number Ticker
Counts up on a spring the first time it scrolls into view, writing straight to the DOM.
- Uptime this year
- 0.00%
- Median response
- 0 ms
- Deploys shipped
- 0
Installation
npx shadcn@latest add https://ui-collections.vercel.app/r/tide/number-ticker.jsonAlso installs the theme, cn(). Packages: clsx, tailwind-merge, motion.
Usage
number-ticker-demo.tsx
import { NumberTicker } from "@/registry/kit/ui/number-ticker";
const stats = [
{ value: 99.98, decimals: 2, suffix: "%", label: "Uptime this year" },
{ value: 42, suffix: " ms", label: "Median response" },
{ value: 1283, label: "Deploys shipped" },
];
export default function NumberTickerDemo() {
return (
<dl className="grid w-full max-w-3xl gap-8 text-center sm:grid-cols-3">
{stats.map((stat) => (
<div key={stat.label} className="flex flex-col-reverse">
<dt className="mt-2 text-sm text-muted">{stat.label}</dt>
<dd className="font-display text-6xl tracking-[-0.03em]">
<NumberTicker value={stat.value} decimals={stat.decimals} suffix={stat.suffix} />
</dd>
</div>
))}
</dl>
);
}API
valuenumberTarget value.
from= 0
numberStart value, rendered before hydration.
decimals= 0
numberFraction digits.
locale= "en-US"
stringIntl locale for formatting.
prefix / suffixstringText around the number.
Source
components/ui/number-ticker.tsx
"use client";
import { useInView, useMotionValue, useSpring } from "motion/react";
import { useEffect, useMemo, useRef } from "react";
import { cn } from "@/registry/kit/lib/utils";
export interface NumberTickerProps {
value: number;
/** Starting value, also what renders before hydration. */
from?: number;
decimals?: number;
/** Fixed by default so server and client format identically. */
locale?: string;
prefix?: string;
suffix?: string;
className?: string;
}
/** Counts up to `value` on a spring the first time it scrolls into view. Updates the DOM directly, not React state. */
export function NumberTicker({
value,
from = 0,
decimals = 0,
locale = "en-US",
prefix,
suffix,
className,
}: NumberTickerProps) {
const ref = useRef<HTMLSpanElement>(null);
const target = useMotionValue(from);
const spring = useSpring(target, { stiffness: 55, damping: 18, restDelta: 0.001 });
const inView = useInView(ref, { once: true, margin: "0px 0px -10% 0px" });
const format = useMemo(
() => new Intl.NumberFormat(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }),
[locale, decimals],
);
useEffect(
() =>
spring.on("change", (v) => {
if (ref.current) ref.current.textContent = format.format(v);
}),
[spring, format],
);
useEffect(() => {
if (!inView) return;
target.set(value);
}, [inView, value, target]);
return (
<span className={cn("tabular-nums", className)}>
{prefix}
<span ref={ref}>{format.format(from)}</span>
{suffix}
</span>
);
}