Number Ticker
Transitions numeriques animees pour les statistiques, les KPI et les mises en avant numeriques compactes. Installez Number Ticker depuis le registre VLLNT UI avec la CLI shadcn.
Transitions numeriques animees pour les statistiques, les KPI et les mises en avant numeriques compactes. Fait partie de la famille utility dans VLLNT UI, il est publie comme entree de registre lisible par machine — copiez la source directement dans votre application avec la CLI shadcn et possedez-la, sans dependance d'execution a une bibliotheque de composants.
Apercu
Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.
Installation
Ajoutez Number Ticker a votre projet avec la CLI shadcn. La source arrive dans votre code, prete a etre adaptee :
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/number-ticker.jsonSource
"use client";
import * as React from "react";
import { cn } from "../../lib/utils";
export type NumberTickerProps = React.ComponentPropsWithoutRef<"span"> & {
delay?: number;
duration?: number;
formatOptions?: Intl.NumberFormatOptions;
from?: number;
locale?: string;
value: number;
};
const NUMBER_FORMATTER_CACHE = new Map<string, Intl.NumberFormat>();
function getNumberTickerFormatter(
locale: string | undefined,
formatOptions: Intl.NumberFormatOptions | undefined,
): Intl.NumberFormat {
const key = `${locale ?? ""}|${formatOptions ? JSON.stringify(formatOptions) : ""}`;
let formatter = NUMBER_FORMATTER_CACHE.get(key);
if (!formatter) {
formatter = Intl.NumberFormat(locale, formatOptions);
NUMBER_FORMATTER_CACHE.set(key, formatter);
}
return formatter;
}
export const NumberTicker = ({
className,
delay = 0,
duration = 1.2,
formatOptions,
from = 0,
locale,
ref,
value,
...props
}: NumberTickerProps & { ref?: React.Ref<HTMLSpanElement> }) => {
const [currentValue, setCurrentValue] = React.useState(() => from);
React.useEffect(() => {
const reducedMotion =
typeof window !== "undefined" &&
typeof window.matchMedia === "function" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reducedMotion || duration <= 0) {
setCurrentValue(value);
return;
}
let animationFrame = 0;
let timeoutId = 0;
const startDelay = Math.max(0, delay * 1000);
const durationMs = duration * 1000;
timeoutId = window.setTimeout(() => {
const startTime = performance.now();
const tick = (timestamp: number) => {
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / durationMs, 1);
const nextValue = from + (value - from) * progress;
setCurrentValue(nextValue);
if (progress < 1) {
animationFrame = window.requestAnimationFrame(tick);
}
};
animationFrame = window.requestAnimationFrame(tick);
}, startDelay);
return () => {
window.clearTimeout(timeoutId);
window.cancelAnimationFrame(animationFrame);
};
}, [delay, duration, from, value]);
const formatter = getNumberTickerFormatter(locale, formatOptions);
return (
<span
className={cn("tabular-nums tracking-tight", className)}
ref={ref}
{...props}
>
{formatter.format(currentValue)}
</span>
);
};
NumberTicker.displayName = "NumberTicker";
Stories
Explorez chaque variante et etat dans le Storybook interactif :
Apercu
Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.