StatCard
Metric card with optional progress ring, loading and errored states.
import { StatCard }from "@/components/ui"
With & without ring
Full source
The complete component, read from src/components/ui/stat-card.tsx.
stat-card.tsx
"use client";
import { cn } from "@/lib/utils";
type StatTone = "pine" | "amber" | "rose" | "neutral";
interface StatCardProps {
label: string;
value: string;
sub?: string;
tone?: StatTone;
ring?: number;
loading?: boolean;
errored?: boolean;
onClick?: () => void;
compact?: boolean;
}
const TONE_TEXT: Record<StatTone, string> = {
pine: "text-accent",
amber: "text-warning",
rose: "text-error",
neutral: "text-text",
};
const TONE_STROKE: Record<StatTone, string> = {
pine: "var(--color-accent)",
amber: "var(--color-warning)",
rose: "var(--color-error)",
neutral: "var(--color-text)",
};
export function StatCard({
label,
value,
sub,
tone = "neutral",
ring: ringValue,
loading,
errored,
onClick,
compact,
}: StatCardProps) {
return (
<button
onClick={onClick}
disabled={loading || errored}
className={cn(
"relative flex items-center rounded-lg border border-border bg-surface text-left transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent",
"disabled:cursor-default",
compact ? "gap-3 p-3" : "gap-4 p-4"
)}
>
{ringValue !== undefined && (
<span className={cn("relative flex shrink-0 items-center justify-center", compact ? "h-8 w-8" : "h-11 w-11")}>
<svg viewBox="0 0 40 40" className={cn("-rotate-90", compact ? "h-8 w-8" : "h-11 w-11")}>
<circle cx="20" cy="20" r="16" fill="none" stroke="var(--color-bg-secondary)" strokeWidth="4" />
{!loading && !errored && (
<circle
cx="20" cy="20" r="16" fill="none" stroke={TONE_STROKE[tone]} strokeWidth="4" strokeLinecap="round"
strokeDasharray={`${(ringValue / 100) * 100.5} 100.5`}
/>
)}
</svg>
</span>
)}
<span className="min-w-0 flex-1">
<span className="block text-xs font-medium uppercase tracking-wider text-text-secondary">{label}</span>
{loading ? (
<span className={cn("block animate-pulse rounded bg-bg-secondary", compact ? "mt-1 h-5 w-16" : "mt-1.5 h-6 w-20")} />
) : (
<span
className={cn(
"mt-0.5 block font-semibold tabular-nums",
compact ? "text-xl" : "text-2xl",
errored ? "text-text-tertiary" : TONE_TEXT[tone]
)}
>
{errored ? "\u2014" : value}
</span>
)}
{!compact && (
<span className={cn("mt-0.5 block truncate text-xs", errored ? "text-error" : "text-text-secondary")}>
{loading ? "\u00A0" : errored ? "Couldn't load \u00B7 Retry" : sub}
</span>
)}
</span>
</button>
);
}