Search

Search the site

All components

Live Counter

Animated numeric display for live metrics. Pass `value` and optional `previousValue`; trend is derived when `showTrend` is enabled.

Default
Compact KPI card with formatted value and up trend.
128.3K

Previous: 128.3K | Current: 128.3K

1"use client";
2
3import { useEffect, useState } from "react";
4import { LiveCounter, formatCompact } from "@/registry/ui";
5import { Card, CardContent } from "@/components/ui/card";
6
7const useLiveDemoCounter = (initialValue = 128_340) => {
8 const [counterState, setCounterState] = useState({
9 value: initialValue,
10 previousValue: initialValue,
11 });
12
13 useEffect(() => {
14 const id = setInterval(() => {
15 setCounterState((prev) => {
16 const delta = Math.round((Math.random() - 0.35) * 2200);
17 return {
18 previousValue: prev.value,
19 value: Math.max(95_000, prev.value + delta),
20 };
21 });
22 }, 1500);
23
24 return () => clearInterval(id);
25 }, []);
26
27 return counterState;
28}
29
30export function LiveCounterDefaultExample() {
31 const { value, previousValue } = useLiveDemoCounter();
32
33 return (
34 <Card>
35 <CardContent className="space-y-4">
36 <LiveCounter
37 value={value}
38 previousValue={previousValue}
39 label="Active Users"
40 formatValue={formatCompact}
41 suffix="+"
42 showTrend
43 size="xl"
44 />
45
46 <p className="font-mono text-xs text-muted-foreground">
47 Previous: {formatCompact(previousValue)} | Current:{" "}
48 {formatCompact(value)}
49 </p>
50 </CardContent>
51 </Card>
52 );
53}
Size variants
Compact KPI card with formatted value and up trend.
128.3K
128.3K
128.3K
128.3K
128.3K
128.3K
1"use client";
2
3import { useEffect, useState } from "react";
4import { LiveCounter, formatCompact } from "@/registry/ui";
5import { Card, CardContent } from "@/components/ui/card";
6
7const useLiveDemoCounter = (initialValue = 128_340) => {
8 const [counterState, setCounterState] = useState({
9 value: initialValue,
10 previousValue: initialValue,
11 });
12
13 useEffect(() => {
14 const id = setInterval(() => {
15 setCounterState((prev) => {
16 const delta = Math.round((Math.random() - 0.35) * 2200);
17 return {
18 previousValue: prev.value,
19 value: Math.max(95_000, prev.value + delta),
20 };
21 });
22 }, 1500);
23
24 return () => clearInterval(id);
25 }, []);
26
27 return counterState;
28}
29
30export function LiveCounterSizeExample() {
31 const { value, previousValue } = useLiveDemoCounter();
32 const sizes = ["xs", "sm", "md", "lg", "xl", "2xl"] as const;
33
34 return (
35 <Card className="w-full">
36 <CardContent className="grid grid-cols-1 gap-3 md:grid-cols-2">
37 {sizes.map((size) => (
38 <div
39 key={size}
40 className="min-w-0 rounded-lg border border-border/60 bg-muted/20 p-4"
41 >
42 <LiveCounter
43 value={value}
44 previousValue={previousValue}
45 label={size}
46 formatValue={formatCompact}
47 suffix="+"
48 showTrend
49 size={size}
50 className="max-w-[18rem]"
51 />
52 </div>
53 ))}
54 </CardContent>
55 </Card>
56 );
57}
Trend variants
Compact KPI card with formatted value and up trend.
129.3K
127.3K
128.3K
128.3K

glow off

1"use client";
2
3import { useEffect, useState } from "react";
4import { LiveCounter, formatCompact } from "@/registry/ui";
5import { Card, CardContent } from "@/components/ui/card";
6
7const TREND_VARIANTS = [
8 {
9 key: "up",
10 label: "Up",
11 getValue: (value: number) => value + 1000,
12 getPrevious: (value: number) => value,
13 glow: true,
14 },
15 {
16 key: "down",
17 label: "Down",
18 getValue: (value: number) => value - 1000,
19 getPrevious: (value: number) => value,
20 glow: true,
21 },
22 {
23 key: "neutral",
24 label: "Neutral",
25 getValue: (value: number) => value,
26 getPrevious: (value: number) => value,
27 glow: true,
28 },
29 {
30 key: "live",
31 label: "Live",
32 getValue: (value: number) => value,
33 getPrevious: (_value: number, previousValue: number) => previousValue,
34 glow: false,
35 },
36] as const;
37
38const useLiveDemoCounter = (initialValue = 128_340) => {
39 const [counterState, setCounterState] = useState({
40 value: initialValue,
41 previousValue: initialValue,
42 });
43
44 useEffect(() => {
45 const id = setInterval(() => {
46 setCounterState((prev) => {
47 const delta = Math.round((Math.random() - 0.35) * 2200);
48 return {
49 previousValue: prev.value,
50 value: Math.max(95_000, prev.value + delta),
51 };
52 });
53 }, 1500);
54
55 return () => clearInterval(id);
56 }, []);
57
58 return counterState;
59};
60
61export function LiveCounterTrendExample() {
62 const { value, previousValue } = useLiveDemoCounter();
63
64 return (
65 <Card className="w-full">
66 <CardContent className="grid grid-cols-1 gap-3 sm:grid-cols-2">
67 {TREND_VARIANTS.map((variant) => (
68 <div
69 key={variant.key}
70 className="min-w-0 rounded-lg border border-border/60 bg-muted/20 p-4"
71 >
72 <LiveCounter
73 value={variant.getValue(value)}
74 previousValue={variant.getPrevious(value, previousValue)}
75 label={variant.label}
76 formatValue={formatCompact}
77 showTrend
78 glow={variant.glow}
79 size="lg"
80 />
81 {variant.key === "live" && (
82 <p className="mt-2 font-mono text-[10px] text-muted-foreground uppercase tracking-wide">
83 glow off
84 </p>
85 )}
86 </div>
87 ))}
88 </CardContent>
89 </Card>
90 );
91}

Installation & source

Install via the shadcn CLI or copy the registry files manually.

bash
npx shadcn@latest add @tt-ui/live-counter

Props

NameTypeDefaultDescription
valuenumberrequiredCurrent numeric value to display
previousValuenumberundefinedPrior value used to derive trend when `showTrend` is true
formatValue(value: number) => stringformatWithCommasFormatter for the displayed number
prefixReactNodeundefinedText or node before the number
suffixReactNodeundefinedText or node after the number
sizeLiveCounterSize"md"Typography scale: xs through 2xl
labelstringundefinedOptional label rendered above the value
showTrendbooleanfalseShow trend icon when `previousValue` is provided (derived from value vs previousValue)
animationDurationnumber800Count animation duration in milliseconds
glowbooleantrueBrief glow pulse when value changes
classNamestringundefinedClasses on the outer shell
numberClassNamestringundefinedClasses on the number element
aria-labelstringundefinedAccessible label for screen readers