Donut chart

A donut whose arcs morph between datasets, with the active value rolling into the center.

pnpm dlx shadcn@latest add @uiarc/donut-chart
Live · keyboard ready

Visits by source

Aug 23 – Sep 21, 2026

Visits by source, Aug 23 – Sep 21, 2026. Visits 40,610. Search, 16,840, 41%. Direct, 8,120, 20%. Newsletter, 5,260, 13%. Referrals, 6,430, 16%. Social, 2,910, 7%. Other, 1,050, 3%. Includes Podcasts, Partners.

Visits by source, Aug 23 – Sep 21, 2026
SegmentValueShare
Search16,84041%
Direct8,12020%
Newsletter5,26013%
Referrals6,43016%
Social2,9107%
Podcasts (Other)6402%
Partners (Other)4101%
  • Traffic by source, spend by category, or storage by file type.
  • A dashboard card where the total and one highlighted share matter most.
  • Switching between datasets, such as this month and last month, with the same categories.
  • Use bar-chart when precise comparison between parts matters or there are many parts.
  • Use gauge or usage-meter for a single value against a limit.
  • Use line-chart for change over time.

Installation

Add Donut chart with the shadcn CLI, or copy the source by hand.

pnpm dlx shadcn@latest add @uiarc/donut-chart

Adds the component and its local dependencies, and installs motion. First time? Add the @uiarc registry to components.json, or use the full URL:

example.tsx
import { DonutChart } from "@/registry/components/donut-chart/donut-chart"; export function TrafficSources() {  return (    <DonutChart      label="Visits by source"      unit="visits"      data={[        { key: "search", label: "Search", value: 4210 },        { key: "direct", label: "Direct", value: 2380 },        { key: "social", label: "Social", value: 1190 },        { key: "email", label: "Email", value: 640 },        { key: "ads", label: "Ads", value: 120 },        { key: "other", label: "Referral", value: 90 },      ]}    />  );}

DonutChart

A donut chart with a synced legend that shows and hides segments, a rolling center readout, automatic Other grouping, and arcs that morph between datasets.

PropTypeDefaultDescription
dataRequiredDonutChartDatum[]–Parts: { key, label, value, color? }. Values of zero or less are left out.
labelRequiredstring–What the whole is, such as "Visits by source". Names the chart for assistive technology.
unitstring""Unit after values, such as "visits". Shown under the total at rest.
formatValue(value: number) => stringgrouped numberFormats values in the center, legend, and table.
totalLabelstring"Total"Center label at rest, above the total.
sizenumber208Diameter in pixels. The chart scales down to fit narrower containers.
thicknessnumber24Ring thickness in pixels.
groupBelownumber0.04Parts below this share of the total join Other, when at least two would.
maxSegmentsnumber6The most segments drawn, counting Other. The smallest parts beyond it are grouped.
otherLabelstring"Other"Label of the grouped segment.
activeKeystring | null–Controlled selected segment key. Hover and focus preview other segments without changing it.
defaultActiveKeystring | nullnullSelected segment on first render when uncontrolled.
onActiveChange(key: string | null) => void–Called when a segment is pinned or unpinned by clicking the ring, or a legend row when legendAction is select.
hiddenKeysstring[]–Controlled hidden segment keys. A hidden segment closes and the rest of the ring redistributes.
defaultHiddenKeysstring[][]Hidden segments on first render when uncontrolled.
onHiddenKeysChange(keys: string[]) => void–Called when a legend row shows or hides its segment.
legendAction"toggle" | "select""toggle"What clicking a legend row does: show or hide its segment, or pin it as the selected segment.
legendbooleantrueThe synced legend beside or below the ring.
emptyLabelstring"No data yet"Center text when the total is zero.
refRef<HTMLElement>–The figure element.
classNamestring–Class on the figure.
ArrowDownorArrowRight
Moves to the next legend row, looping at the end.
ArrowUporArrowLeft
Moves to the previous legend row.
HomeorEnd
Jumps to the first or last row.
EnterorSpace
Shows or hides the focused segment; with legendAction select, pins or unpins it.
Escape
Clears the pinned segment.
Arrow keys on the ring
Without a legend the ring takes focus: arrows walk the segments, Enter pins one, and each is announced.
  • The legend is the keyboard path: each row is a button with aria-pressed (shown, or pinned with legendAction select) and a label giving value, share, and grouped members.
  • Showing or hiding a segment is announced in a polite live region with the new total. Without a legend the ring is focusable and announces each segment as the arrows reach it.
  • The ring and center readout are aria-hidden; a hidden summary and table list every part, including the members of Other.
  • Keyboard focus previews a segment in the ring; hover previews with a mouse only. No focus rings are drawn.
  • One sweep the first time the chart scrolls into view: every segment leaves the top together and each trailing edge follows a beat behind the one before.
  • New data and hidden segments morph start and end angles, never paths, from wherever each arc is on screen. Springs keep their velocity, so an interrupted change continues smoothly. Segments keep their order and never remount mid-morph.
  • The active segment slides out along its middle and thickens slightly while the others dim. One pointer handler hit-tests the angle, so crossing a gap never drops the hover.
  • The center readout rolls like a drum toward the active segment with tabular numbers in a fixed cell; totals and shares count to new values.
  • Reduced motion jumps arcs, lifts, and text to their final state.
  • The figure is a container: at 460px and wider the legend sits beside the ring, below that it stacks under it.
  • The ring scales down from size to fit narrower containers while keeping its aspect ratio.
  • Legend hover styles only apply on hover-capable fine pointers; taps pin a segment.
  • Arc angles and lifts are motion values; one batched paint per frame writes every path d and transform straight to the DOM, with no React render per frame.
  • Counting numbers write their text directly; center readouts stay mounted, so hovering never mounts or unmounts nodes.

Notes for AI

Give your coding assistant the Markdown reference instead of screenshots.

  • Use for two to six parts of one whole. With more parts, lean on groupBelow and maxSegments or use bar-chart.
  • Keep keys stable between datasets so arcs morph in place.
  • Leave color out to get the shared --series-1 to --series-4 palette in data order, then neutral steps; a key keeps its color across datasets. Pass colors only when they carry meaning.
  • Legend rows show and hide segments by default; set legendAction to select for the older pin behavior.
  • Drive activeKey from a table or filter to highlight the same category elsewhere.

The full library index for assistants is at /llms.txt.