All Taste components
Animated charts
Sweeping donut
A share-of-total ring that sweeps round in one continuous stroke, segment by segment, beside its legend.
4 segments · continuous sweep · centre total · legend bars · stacks on narrow widths
Orders by channelLast 30 days
1,284orders
- Website539 · 42%
- Mobile app360 · 28%
- Marketplace231 · 18%
- In store154 · 12%
/* Add once to globals.css. Each arc grows from nothing to its share; the
delays line up so the ring sweeps round in one stroke:
@keyframes sweep { from { stroke-dasharray: 0 1; } }
*/
const channels = [
{ label: "Website", orders: 539, tone: "var(--tst-primary)" },
{ label: "Mobile app", orders: 360, tone: "color-mix(in srgb, var(--tst-primary) 62%, var(--tst-bg))" },
{ label: "Marketplace", orders: 231, tone: "color-mix(in srgb, var(--tst-primary) 38%, var(--tst-bg))" },
{ label: "In store", orders: 154, tone: "color-mix(in srgb, var(--tst-primary) 20%, var(--tst-bg))" },
];
const SWEEP = 1.2; // seconds for the full ring
const GAP = 0.006; // hairline between segments
export function SweepingDonut() {
const total = channels.reduce((s, c) => s + c.orders, 0);
const top = Math.max(...channels.map((c) => c.orders));
let start = 0;
const arcs = channels.map((c) => {
const share = c.orders / total;
const arc = { ...c, share, start };
start += share;
return arc;
});
return (
<section aria-label="Orders by channel" className="mx-auto grid max-w-xl gap-5 rounded-lg border border-border bg-bg p-[18px] font-sans">
<div>
<p className="text-xs font-medium text-fg-subtle">Orders by channel</p>
<p className="text-[13.5px] text-fg-muted">Last 30 days</p>
</div>
<div className="grid items-center gap-6 sm:grid-cols-[180px_1fr] sm:gap-8">
<div className="relative mx-auto w-[180px]">
<svg viewBox="0 0 120 120" className="block h-auto w-full -rotate-90" aria-hidden>
<circle cx="60" cy="60" r="48" fill="none" className="stroke-surface-2" strokeWidth="14" />
{arcs.map((a) => (
<circle
key={a.label}
cx="60" cy="60" r="48" pathLength={1} fill="none" strokeWidth="14" stroke={a.tone}
className="animate-[sweep_linear_both] motion-reduce:animate-none"
style={{
strokeDasharray: a.share - GAP + " 1",
strokeDashoffset: -a.start,
animationDuration: a.share * SWEEP + "s",
animationDelay: 0.15 + a.start * SWEEP + "s",
}}
/>
))}
</svg>
<div className="absolute inset-0 grid place-content-center text-center animate-pop [animation-delay:1250ms] motion-reduce:animate-none">
<span className="text-[28px] font-medium tabular-nums tracking-tight text-fg">{total.toLocaleString("en-IN")}</span>
<span className="text-xs text-fg-subtle">orders</span>
</div>
</div>
<ul className="grid gap-3">
{arcs.map((a, i) => (
<li key={a.label} className="grid grid-cols-[10px_1fr_auto] items-center gap-x-2.5 gap-y-1.5 text-[13px] animate-rise motion-reduce:animate-none" style={{ animationDelay: 300 + i * 70 + "ms" }}>
<i className="size-2.5 rounded-[3px]" style={{ background: a.tone }} />
<span className="text-fg">{a.label}</span>
<span className="tabular-nums text-fg-muted">{a.orders} · {Math.round(a.share * 100)}%</span>
<span className="col-span-2 col-start-2 h-1 overflow-hidden rounded-full bg-surface-2">
<span className="block h-full origin-left rounded-full animate-grow-x motion-reduce:animate-none" style={{ width: (a.orders / top) * 100 + "%", background: a.tone, animationDelay: 450 + i * 70 + "ms" }} />
</span>
</li>
))}
</ul>
</div>
</section>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.