Icons Stat
Original · freeicon stat grid
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/statx-icons.json"use client";
import { useEffect, useRef, useState } from "react";
import type { ComponentType, KeyboardEvent, SVGProps } from "react";
import { motion, useInView, useReducedMotion } from "motion/react";
/* ------------------------------------------------------------------ */
/* Inline SVG icons (no icon libraries) */
/* ------------------------------------------------------------------ */
function IconBase(props: SVGProps<SVGSVGElement>) {
return (
<svg
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
/>
);
}
function IconUsers(props: SVGProps<SVGSVGElement>) {
return (
<IconBase {...props}>
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</IconBase>
);
}
function IconActivity(props: SVGProps<SVGSVGElement>) {
return (
<IconBase {...props}>
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
</IconBase>
);
}
function IconRocket(props: SVGProps<SVGSVGElement>) {
return (
<IconBase {...props}>
<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09Z" />
<path d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2Z" />
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" />
<path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
</IconBase>
);
}
function IconBolt(props: SVGProps<SVGSVGElement>) {
return (
<IconBase {...props}>
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</IconBase>
);
}
function IconShield(props: SVGProps<SVGSVGElement>) {
return (
<IconBase {...props}>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z" />
<path d="m9 12 2 2 4-4" />
</IconBase>
);
}
function IconChart(props: SVGProps<SVGSVGElement>) {
return (
<IconBase {...props}>
<line x1="6" y1="20" x2="6" y2="14" />
<line x1="12" y1="20" x2="12" y2="4" />
<line x1="18" y1="20" x2="18" y2="10" />
</IconBase>
);
}
function IconArrowUp(props: SVGProps<SVGSVGElement>) {
return (
<IconBase {...props}>
<path d="M12 19V5" />
<path d="m5 12 7-7 7 7" />
</IconBase>
);
}
function IconArrowDown(props: SVGProps<SVGSVGElement>) {
return (
<IconBase {...props}>
<path d="M12 5v14" />
<path d="m19 12-7 7-7-7" />
</IconBase>
);
}
/* ------------------------------------------------------------------ */
/* Data model */
/* ------------------------------------------------------------------ */
const PERIODS = [
{ id: "7d", label: "last 7 days" },
{ id: "30d", label: "last 30 days" },
{ id: "90d", label: "last 90 days" },
] as const;
type PeriodId = (typeof PERIODS)[number]["id"];
type Format = "int" | "compact" | "ms" | "percent" | "currency";
type Accent = {
chip: string;
icon: string;
bar: string;
ring: string;
};
type PeriodData = {
value: number;
delta: string;
dir: "up" | "down";
good: boolean;
};
type Metric = {
id: string;
label: string;
hint: string;
format: Format;
decimals: number;
icon: ComponentType<SVGProps<SVGSVGElement>>;
accent: Accent;
series: number[];
data: Record<PeriodId, PeriodData>;
};
const ACCENTS: Record<string, Accent> = {
indigo: {
chip: "bg-indigo-50 dark:bg-indigo-500/10",
icon: "text-indigo-600 dark:text-indigo-300",
bar: "bg-indigo-500/70 dark:bg-indigo-400/70",
ring: "hover:border-indigo-300 dark:hover:border-indigo-500/40",
},
violet: {
chip: "bg-violet-50 dark:bg-violet-500/10",
icon: "text-violet-600 dark:text-violet-300",
bar: "bg-violet-500/70 dark:bg-violet-400/70",
ring: "hover:border-violet-300 dark:hover:border-violet-500/40",
},
sky: {
chip: "bg-sky-50 dark:bg-sky-500/10",
icon: "text-sky-600 dark:text-sky-300",
bar: "bg-sky-500/70 dark:bg-sky-400/70",
ring: "hover:border-sky-300 dark:hover:border-sky-500/40",
},
amber: {
chip: "bg-amber-50 dark:bg-amber-500/10",
icon: "text-amber-600 dark:text-amber-300",
bar: "bg-amber-500/70 dark:bg-amber-400/70",
ring: "hover:border-amber-300 dark:hover:border-amber-500/40",
},
emerald: {
chip: "bg-emerald-50 dark:bg-emerald-500/10",
icon: "text-emerald-600 dark:text-emerald-300",
bar: "bg-emerald-500/70 dark:bg-emerald-400/70",
ring: "hover:border-emerald-300 dark:hover:border-emerald-500/40",
},
rose: {
chip: "bg-rose-50 dark:bg-rose-500/10",
icon: "text-rose-600 dark:text-rose-300",
bar: "bg-rose-500/70 dark:bg-rose-400/70",
ring: "hover:border-rose-300 dark:hover:border-rose-500/40",
},
};
const METRICS: Metric[] = [
{
id: "developers",
label: "Active developers",
hint: "Weekly active accounts across every region",
format: "int",
decimals: 0,
icon: IconUsers,
accent: ACCENTS.indigo,
series: [0.4, 0.5, 0.45, 0.6, 0.55, 0.7, 0.82, 0.94],
data: {
"7d": { value: 12480, delta: "+8.2%", dir: "up", good: true },
"30d": { value: 41900, delta: "+11.4%", dir: "up", good: true },
"90d": { value: 118300, delta: "+18.9%", dir: "up", good: true },
},
},
{
id: "requests",
label: "API requests",
hint: "Successful calls to the REST and GraphQL edge",
format: "compact",
decimals: 0,
icon: IconActivity,
accent: ACCENTS.violet,
series: [0.5, 0.55, 0.6, 0.5, 0.66, 0.72, 0.78, 0.88],
data: {
"7d": { value: 4230000, delta: "+6.1%", dir: "up", good: true },
"30d": { value: 18600000, delta: "+9.7%", dir: "up", good: true },
"90d": { value: 52400000, delta: "+14.2%", dir: "up", good: true },
},
},
{
id: "deploys",
label: "Deploys shipped",
hint: "Production releases that passed CI and rolled out",
format: "int",
decimals: 0,
icon: IconRocket,
accent: ACCENTS.sky,
series: [0.3, 0.5, 0.4, 0.6, 0.72, 0.6, 0.82, 0.9],
data: {
"7d": { value: 1204, delta: "+12.3%", dir: "up", good: true },
"30d": { value: 5380, delta: "+8.9%", dir: "up", good: true },
"90d": { value: 16240, delta: "+15.6%", dir: "up", good: true },
},
},
{
id: "latency",
label: "p95 latency",
hint: "95th-percentile response time at the API gateway",
format: "ms",
decimals: 0,
icon: IconBolt,
accent: ACCENTS.amber,
series: [0.62, 0.5, 0.56, 0.44, 0.5, 0.4, 0.42, 0.38],
data: {
"7d": { value: 128, delta: "-6.4%", dir: "down", good: true },
"30d": { value: 134, delta: "-3.1%", dir: "down", good: true },
"90d": { value: 141, delta: "+2.2%", dir: "up", good: false },
},
},
{
id: "uptime",
label: "Service uptime",
hint: "Availability across all production services",
format: "percent",
decimals: 2,
icon: IconShield,
accent: ACCENTS.emerald,
series: [0.85, 0.9, 0.88, 0.92, 0.95, 0.93, 0.97, 0.99],
data: {
"7d": { value: 99.98, delta: "+0.01 pts", dir: "up", good: true },
"30d": { value: 99.96, delta: "+0.02 pts", dir: "up", good: true },
"90d": { value: 99.95, delta: "-0.01 pts", dir: "down", good: false },
},
},
{
id: "mrr",
label: "Recurring revenue",
hint: "Net new MRR booked inside this window",
format: "currency",
decimals: 0,
icon: IconChart,
accent: ACCENTS.rose,
series: [0.4, 0.45, 0.5, 0.55, 0.62, 0.7, 0.8, 0.92],
data: {
"7d": { value: 48200, delta: "+4.1%", dir: "up", good: true },
"30d": { value: 186400, delta: "+7.3%", dir: "up", good: true },
"90d": { value: 540900, delta: "+12.8%", dir: "up", good: true },
},
},
];
/* ------------------------------------------------------------------ */
/* Formatting helpers */
/* ------------------------------------------------------------------ */
function formatCompact(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
return Math.round(n).toString();
}
function formatValue(n: number, format: Format, decimals: number): string {
switch (format) {
case "int":
return Math.round(n).toLocaleString("en-US");
case "compact":
return formatCompact(n);
case "ms":
return `${Math.round(n)} ms`;
case "percent":
return `${n.toFixed(decimals)}%`;
case "currency":
return `$${Math.round(n).toLocaleString("en-US")}`;
default:
return String(n);
}
}
/* ------------------------------------------------------------------ */
/* Count-up hook (respects reduced motion, retargets on change) */
/* ------------------------------------------------------------------ */
function useCountUp(
target: number,
active: boolean,
reduced: boolean,
): number {
const [display, setDisplay] = useState(0);
const fromRef = useRef(0);
const rafRef = useRef<number | null>(null);
useEffect(() => {
if (!active) return;
if (reduced) {
fromRef.current = target;
setDisplay(target);
return;
}
const from = fromRef.current;
const duration = 950;
const startTime = performance.now();
const tick = (now: number) => {
const t = Math.min(1, (now - startTime) / duration);
const eased = 1 - Math.pow(1 - t, 3);
const current = from + (target - from) * eased;
fromRef.current = current;
setDisplay(current);
if (t < 1) {
rafRef.current = requestAnimationFrame(tick);
}
};
rafRef.current = requestAnimationFrame(tick);
return () => {
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
};
}, [target, active, reduced]);
return display;
}
/* ------------------------------------------------------------------ */
/* Delta pill */
/* ------------------------------------------------------------------ */
function Delta({ data }: { data: PeriodData }) {
const tone = data.good
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-300"
: "bg-rose-50 text-rose-700 dark:bg-rose-500/10 dark:text-rose-300";
return (
<span
className={`inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-semibold tabular-nums ${tone}`}
>
{data.dir === "up" ? (
<IconArrowUp className="h-3 w-3" strokeWidth={2.25} />
) : (
<IconArrowDown className="h-3 w-3" strokeWidth={2.25} />
)}
<span className="sr-only">
{data.good ? "improved, " : "declined, "}
{data.dir === "up" ? "up " : "down "}
</span>
{data.delta}
</span>
);
}
/* ------------------------------------------------------------------ */
/* Stat card */
/* ------------------------------------------------------------------ */
function StatCard({
metric,
period,
periodLabel,
inView,
reduced,
index,
}: {
metric: Metric;
period: PeriodId;
periodLabel: string;
inView: boolean;
reduced: boolean;
index: number;
}) {
const d = metric.data[period];
const display = useCountUp(d.value, inView, reduced);
const Icon = metric.icon;
return (
<motion.div
className={`group relative flex flex-col gap-5 rounded-2xl border border-slate-200 bg-white p-6 transition-colors duration-300 dark:border-slate-800 dark:bg-slate-900/50 ${metric.accent.ring} hover:shadow-lg hover:shadow-slate-900/5 dark:hover:shadow-black/30`}
initial={reduced ? false : { opacity: 0, y: 16 }}
animate={
reduced
? { opacity: 1, y: 0 }
: inView
? { opacity: 1, y: 0 }
: { opacity: 0, y: 16 }
}
transition={{ duration: 0.5, delay: index * 0.06, ease: "easeOut" }}
>
<div className="flex items-start justify-between">
<span
className={`flex h-11 w-11 items-center justify-center rounded-xl ${metric.accent.chip}`}
>
<Icon className={`h-5 w-5 ${metric.accent.icon}`} />
</span>
<Delta data={d} />
</div>
<div>
<div className="text-3xl font-semibold tracking-tight tabular-nums text-slate-900 dark:text-white sm:text-4xl">
{formatValue(display, metric.format, metric.decimals)}
</div>
<div className="mt-1 text-sm font-medium text-slate-500 dark:text-slate-400">
{metric.label}
</div>
</div>
<div
className="flex h-12 items-end gap-1"
aria-hidden="true"
>
{metric.series.map((v, i) => (
<motion.span
key={i}
className={`flex-1 rounded-sm ${metric.accent.bar}`}
style={{ height: `${Math.max(10, v * 100)}%`, transformOrigin: "bottom" }}
initial={reduced ? false : { scaleY: 0, opacity: 0 }}
animate={
reduced
? { scaleY: 1, opacity: 1 }
: inView
? { scaleY: 1, opacity: 1 }
: { scaleY: 0, opacity: 0 }
}
transition={{
duration: 0.5,
delay: 0.15 + index * 0.06 + i * 0.04,
ease: "easeOut",
}}
/>
))}
</div>
<div className="border-t border-slate-100 pt-4 dark:border-slate-800/80">
<p className="text-xs leading-relaxed text-slate-500 dark:text-slate-400">
{metric.hint}
</p>
<p className="mt-1 text-[11px] font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500">
vs previous {periodLabel.replace("last ", "")}
</p>
</div>
</motion.div>
);
}
/* ------------------------------------------------------------------ */
/* Main component */
/* ------------------------------------------------------------------ */
export default function StatxIcons() {
const reduced = useReducedMotion() ?? false;
const [period, setPeriod] = useState<PeriodId>("30d");
const gridRef = useRef<HTMLDivElement>(null);
const inView = useInView(gridRef, { once: true, margin: "-80px" });
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
const activeLabel =
PERIODS.find((p) => p.id === period)?.label ?? PERIODS[0].label;
const onTabKeyDown = (
event: KeyboardEvent<HTMLButtonElement>,
index: number,
) => {
let next = index;
if (event.key === "ArrowRight" || event.key === "ArrowDown") {
next = (index + 1) % PERIODS.length;
} else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
next = (index - 1 + PERIODS.length) % PERIODS.length;
} else if (event.key === "Home") {
next = 0;
} else if (event.key === "End") {
next = PERIODS.length - 1;
} else {
return;
}
event.preventDefault();
setPeriod(PERIODS[next].id);
tabRefs.current[next]?.focus();
};
return (
<section
aria-labelledby="statx-heading"
className="relative w-full overflow-hidden bg-white px-6 py-20 text-slate-900 dark:bg-slate-950 dark:text-white sm:py-24 lg:px-8"
>
<style>{`
@keyframes statx-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.3; transform: scale(1.9); }
}
.statx-live-dot { animation: statx-pulse 2.4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.statx-live-dot { animation: none; }
}
`}</style>
{/* atmospheric accent */}
<div
aria-hidden="true"
className="pointer-events-none absolute -top-24 left-1/2 h-72 w-[42rem] -translate-x-1/2 rounded-full bg-gradient-to-r from-indigo-200/40 via-violet-200/30 to-sky-200/40 blur-3xl dark:from-indigo-600/10 dark:via-violet-600/10 dark:to-sky-600/10"
/>
<div className="relative mx-auto max-w-6xl">
{/* header */}
<div className="flex flex-col gap-8 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-xl">
<div className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-medium text-slate-600 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300">
<span className="relative flex h-2 w-2">
<span className="statx-live-dot absolute inline-flex h-full w-full rounded-full bg-emerald-400" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
</span>
Live · updated just now
</div>
<h2
id="statx-heading"
className="mt-4 text-3xl font-semibold tracking-tight text-slate-900 dark:text-white sm:text-4xl"
>
Everything moving through your stack, at a glance
</h2>
<p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-400">
Six core signals, refreshed every minute. Switch the window to see
how growth, reliability, and revenue trend together.
</p>
</div>
{/* segmented control (radiogroup) */}
<div className="shrink-0">
<div
id="statx-window-label"
className="mb-2 text-xs font-medium uppercase tracking-wide text-slate-400 dark:text-slate-500"
>
Time window
</div>
<div
role="radiogroup"
aria-labelledby="statx-window-label"
className="inline-flex rounded-full border border-slate-200 bg-slate-100 p-1 dark:border-slate-800 dark:bg-slate-900"
>
{PERIODS.map((p, i) => {
const selected = p.id === period;
return (
<button
key={p.id}
ref={(el) => {
tabRefs.current[i] = el;
}}
type="button"
role="radio"
aria-checked={selected}
tabIndex={selected ? 0 : -1}
onClick={() => setPeriod(p.id)}
onKeyDown={(e) => onTabKeyDown(e, i)}
className={`rounded-full px-4 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-100 dark:focus-visible:ring-offset-slate-900 ${
selected
? "bg-white text-slate-900 shadow-sm dark:bg-slate-950 dark:text-white"
: "text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
}`}
>
{p.label.replace("last ", "")}
</button>
);
})}
</div>
</div>
</div>
<p className="sr-only" role="status" aria-live="polite">
Showing platform metrics for the {activeLabel}.
</p>
{/* grid */}
<div
ref={gridRef}
className="mt-12 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"
>
{METRICS.map((metric, index) => (
<StatCard
key={metric.id}
metric={metric}
period={period}
periodLabel={activeLabel}
inView={inView}
reduced={reduced}
index={index}
/>
))}
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Four Stat Metric Band
OriginalA four-column metric band with gradient figures inside a hairline-divided card, ideal for headline numbers like uptime, latency and growth.

Labelled Stats With Dividers
OriginalThree centred stats separated by responsive dividers, each pairing a headline figure with an uppercase label and supporting detail.

Trusted By Logo Cloud
OriginalA responsive six-up logo strip using simple inline-SVG marks and wordmarks with a grayscale-to-colour hover, for a trusted-by section.

Hero Stat With Supporting Copy
OriginalA two-column layout pairing one oversized hero statistic with a supporting paragraph and a checklist card explaining the number.

Count-up stats reveal
OriginalA four-card stat band whose gradient numbers count up from zero as the section scrolls into view, with staggered blur-in cards, a pulsing eyebrow, floating aurora blobs and a shimmering underline sweep.

Animated SVG progress rings
OriginalFour circular SVG progress rings that draw their gradient arcs and count up to their target percentage the moment they enter view, over a slowly rotating conic halo with a soft glow pulse behind each ring.

Staggered stat band with marquee ticker
OriginalA dark stat band where 3D-tilt cards stagger into view with count-up figures over an animated gradient mesh, finished by an infinite marquee ticker of highlight chips that pauses on hover.

Counter Row Stat
Originalanimated counter stats row

Cards Stat
Originalstat cards with icons

Gradient Stat
Originalgradient stats band

Split Stat
Originalstats beside copy

Progress Stat
Originalstats with progress rings

