All Taste components
Dashboard widgets
Donut chart card
Where visits came from, as a donut with the total in the middle and a legend with shares.
4 segments · centre total · legend with counts and %
Traffic sources
Last 30 days
12,480visits
- Direct5,24242%
- Search3,49428%
- Referral2,24618%
- Social1,49812%
export interface Slice { label: string; value: number }
const sample: Slice[] = [
{ label: "Direct", value: 5242 },
{ label: "Search", value: 3494 },
{ label: "Referral", value: 2246 },
{ label: "Social", value: 1498 },
];
/** Strongest tone first, the last slice neutral — all from the theme variables. */
const tones = [
"var(--tst-primary)",
"color-mix(in srgb, var(--tst-primary) 62%, var(--tst-bg))",
"color-mix(in srgb, var(--tst-primary) 34%, var(--tst-bg))",
"var(--tst-border-strong)",
];
export function DonutChartCard({ data = sample, title = "Traffic sources", unit = "visits" }:
{ data?: Slice[]; title?: string; unit?: string }) {
const total = data.reduce((s, d) => s + d.value, 0);
let offset = 0;
const arcs = data.map((d) => {
const pct = (d.value / total) * 100;
const arc = { ...d, pct, offset };
offset += pct;
return arc;
});
return (
<section className="w-full rounded-lg border border-border bg-bg p-4">
<div className="flex items-start justify-between gap-3">
<div>
<h3 className="text-sm font-medium">{title}</h3>
<p className="text-[12.5px] text-fg-subtle">Last 30 days</p>
</div>
<button className="h-8 rounded border border-border-strong px-2.5 text-[13px] font-medium hover:bg-surface">Export</button>
</div>
<div className="mt-4 flex flex-wrap items-center gap-x-7 gap-y-5">
<div className="relative mx-auto size-36 shrink-0" role="img"
aria-label={arcs.map((a) => a.label + " " + Math.round(a.pct) + "%").join(", ")}>
<svg viewBox="0 0 36 36" className="size-full -rotate-90" aria-hidden>
{arcs.map((a, i) => (
<circle key={a.label} cx="18" cy="18" r="15.915" pathLength={100} fill="none" strokeWidth={3.6}
stroke={tones[i % tones.length]}
strokeDasharray={a.pct - 1 + " " + (101 - a.pct)} strokeDashoffset={-a.offset} />
))}
</svg>
<div className="absolute inset-0 grid place-content-center text-center">
<span className="text-xl font-medium tracking-tight tabular-nums">{total.toLocaleString("en-IN")}</span>
<span className="text-[12.5px] text-fg-subtle">{unit}</span>
</div>
</div>
<ul className="min-w-0 flex-[1_1_180px] divide-y divide-border">
{arcs.map((a, i) => (
<li key={a.label} className="grid grid-cols-[8px_minmax(0,1fr)_auto_44px] items-center gap-2.5 py-2 text-[13px]">
<span className="size-2 rounded-sm" style={{ background: tones[i % tones.length] }} />
<span>{a.label}</span>
<span className="tabular-nums text-fg-muted">{a.value.toLocaleString("en-IN")}</span>
<span className="text-right font-medium tabular-nums">{Math.round(a.pct)}%</span>
</li>
))}
</ul>
</div>
</section>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.