Skip to content
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

  • 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.