All Taste components
Dashboard widgets
KPI row
Four headline numbers with their change since last month and a small trend line.
4 stats · up/down trend · good/bad tone · sparkline
Revenue12.4%
₹8,42,300
vs last month
New customers8.1%
184
vs last month
Avg. order value2.3%
₹2,310
vs last month
Refund rate0.4 pt
1.2%
vs last month
import { ArrowDownRight, ArrowUpRight } from "lucide-react";
import { cn } from "@/lib/utils";
export interface Kpi {
label: string;
value: string;
/** Change since the last period, e.g. 12.4 or -2.3. */
change: number;
/** Shown after the number: "%" or " pt". */
unit?: string;
/** True when a falling number is good news (refunds, churn). */
lowerIsBetter?: boolean;
series: number[];
}
const sample: Kpi[] = [
{ label: "Revenue", value: "₹8,42,300", change: 12.4, series: [22, 25, 24, 28, 27, 31, 30, 34, 33, 37, 39, 42] },
{ label: "New customers", value: "184", change: 8.1, series: [12, 14, 13, 13, 16, 15, 18, 17, 19, 18, 21, 22] },
{ label: "Avg. order value", value: "₹2,310", change: -2.3, series: [30, 31, 29, 30, 28, 29, 27, 28, 27, 26, 27, 26] },
{ label: "Refund rate", value: "1.2%", change: -0.4, unit: " pt", lowerIsBetter: true, series: [9, 8, 9, 7, 8, 6, 7, 6, 5, 6, 5, 4] },
];
function Sparkline({ data }: { data: number[] }) {
const lo = Math.min(...data);
const hi = Math.max(...data);
const points = data
.map((v, i) => (i / (data.length - 1)) * 100 + "," + (92 - ((v - lo) / (hi - lo || 1)) * 84))
.join(" ");
return (
<svg viewBox="0 0 100 100" preserveAspectRatio="none" className="mt-3 h-8 w-full overflow-visible text-primary" aria-hidden>
<polyline points={points} fill="none" stroke="currentColor" strokeWidth={1.5} strokeLinejoin="round" vectorEffect="non-scaling-stroke" />
</svg>
);
}
export function KpiRow({ items = sample, period = "vs last month" }: { items?: Kpi[]; period?: string }) {
return (
<div className="grid w-full gap-3 [grid-template-columns:repeat(auto-fit,minmax(150px,1fr))]">
{items.map((k) => {
const up = k.change >= 0;
const good = k.lowerIsBetter ? !up : up;
const Icon = up ? ArrowUpRight : ArrowDownRight;
return (
<div key={k.label} className="min-w-0 rounded-lg border border-border bg-bg px-4 py-3.5">
<div className="flex items-center justify-between gap-2">
<p className="text-[13px] text-fg-muted">{k.label}</p>
<span
className={cn(
"inline-flex h-5 items-center gap-0.5 rounded-full px-1.5 text-xs font-medium tabular-nums",
good ? "bg-success-soft text-success-fg" : "bg-danger-soft text-danger-fg",
)}
>
<Icon className="size-3" aria-hidden />
<span className="sr-only">{up ? "Up" : "Down"}</span>
{Math.abs(k.change)}{k.unit ?? "%"}
</span>
</div>
<p className="mt-1 text-2xl font-medium tracking-tight tabular-nums text-fg">{k.value}</p>
<p className="text-[12.5px] text-fg-subtle">{period}</p>
<Sparkline data={k.series} />
</div>
);
})}
</div>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.