Donut chart React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
Parts of a whole as a donut or pie, with the total in the middle, a legend with values and shares, and a selectable slice.
The slices sweep in clockwise from the start angle the first time the chart scrolls into view, while the total in the middle counts up. Hover a slice and it lifts, the others dim and the center reads its value, name and share (a pie, or a donut too small for a center, shows a floating readout instead; the folded Other slice lists what is inside it above the chart). Click a slice to select it: it stays lifted and the center keeps its value (click it again, or click the hole, to clear). On a phone, tap a slice to select it (the ring is widened by 12px on each side, so thin rings and slivers are easy to hit), slide a finger along the ring to scrub (the center reads each slice and the one you let go on is selected), tap the hole or outside the ring to clear. Legend rows are buttons: tap or click one to select its slice, hover or focus it to preview the slice in the center. With the keyboard, Tab to the chart: Left / Right (or Up / Down) walk the slices, Home / End jump to the first and last, Enter or Space selects the inspected slice and Escape clears the selection; a live region reads each slice. When the data changes, the slices glide to their new angles. The legend sits beside the chart from a 480px wide container and below it on phones.
Donut chart is a Pro React component in the React chart and stat components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add donut-chart, together with Chart kit, Motion presets, UIProvider, useControllableState and useLocale and the npm packages motion and d3-shape.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add donut-chartDonut chart is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import DonutChart from @/components/ui/donut-chart.
import { DonutChart } from "@/components/ui/donut-chart"
const channels = [
{ id: "direct", label: "Direct sales", value: 48920 },
{ id: "online", label: "Online store", value: 36400 },
{ id: "partners", label: "Partners", value: 22130 },
{ id: "tenders", label: "Public tenders", value: 13600 },
{ id: "other", label: "Other", value: 7400 },
]
export function RevenueByChannel() {
return (
<DonutChart
data={channels}
valueFormat="currency"
locale="en-US"
label="Revenue by channel, September 2026"
onValueChange={(id) => console.log(id)}
/>
)
}Props
data*DonutChartDatum[] = { id: string; label: ReactNode; value: number; color?: string; name?: string }[]One entry per slice. color takes a palette slot ("series-3"), a tone ("success", "danger") or any CSS color; name is the plain-text name when label is not a string. Negative values are ignored.
variantdefault"donut""donut" | "pie"pie has no hole and no center content (the legend gets a total row instead).
sizedefault"md""sm" | "md" | "lg"Diameter 140 / 200 / 260px; it shrinks to the container width.
thicknessdefault0numberRing width in px. 0 = auto (18% of the diameter).
gapdefault2numberpx of surface between slices, measured at the middle of the ring. Slices under 3% give theirs up so they stay visible.
cornerRadiusdefault4numberRounded slice corners in px.
startAngledefault0numberWhere the first slice starts, in degrees: 0 is 12 o'clock, clockwise.
sortdefault"value""value" | "none"value puts the biggest slice first (clockwise from the start angle) and gives it palette slot 1; none keeps the data order.
maxSlicesdefault6numberNamed slices kept; the smaller rest fold into one "Other" slice (labels.other, the neutral gray) whose readout lists them. A single leftover slice is never folded.
colorsstring[]Per-position palette overrides after sorting; a datum's own color wins.
showLegenddefaulttruebooleanThe legend: one button per slice with its value and share.
legendPositiondefault"auto""auto" | "right" | "bottom"auto puts the legend beside the chart when the container is at least 480px wide and below it otherwise (a container query, right in the server html).
showPercentdefaulttruebooleanShares in the legend and the readout.
showValuesdefaulttruebooleanValues in the legend.
centerLabeldefault""ReactNodeThe caption under the center value. Default labels.total ("Total").
centerValueReactNodeReplaces the formatted total ("342"); a trailing unit ("342 invoices") is set smaller.
valuestring | nullThe selected slice id when you control it: lifted, the others dimmed, the center shows its value, name and share. null clears.
defaultValuedefault"null"string | nullThe slice selected at first, uncontrolled.
onValueChange(id: string | null) => voidCalled when a slice or legend row is selected or cleared. The folded slice reports DONUT_CHART_OTHER_ID ("__other__").
valueFormatdefault"number"ChartValueFormat = "number" | "compact" | "currency" | "percent"How values print in the center, legend, readout and table. Percent takes whole numbers (12.4 -> "12.4%" in en-US).
currencydefault"USD"stringISO 4217 code for valueFormat currency, printed with the narrow symbol ("$128,450").
decimalsnumberFraction digits for values; auto by magnitude when unset.
formatValue(value: number, context: { series: string; kind: "tooltip" | "label" | "table" }) => stringReplaces the built-in value formatting everywhere; series is the slice id ("" for the total).
localedefault""stringLocale for numbers. Empty uses the UIProvider / LocaleProvider locale.
showTooltipdefaulttruebooleanThe floating readout for a pie or a donut too small for a center, and the list of folded slices behind Other. A donut with a center reads the inspected slice in the hole either way; selection and announcements work without it.
animateddefaulttruebooleanThe clockwise sweep and count-up on first view, and gliding slices on data changes. Reduced motion always shows the final state.
loadingdefaultfalsebooleanA ring-shaped skeleton and skeleton legend rows in the same box.
emptyStateReactNodeReplaces the built-in empty state (shown when there is no data or the total is 0).
labeldefault""stringAccessible name of the figure (followed by "total ...") and caption of its data table. Empty joins the slice names.
descriptiondefault""stringA one-sentence takeaway for screen readers ("Direct sales bring in 38% of revenue").
surfacedefault""stringThe color behind the chart. Empty uses the surface token; the gaps between slices show whatever is behind the chart.
labelsPartial<DonutChartLabels>Strings: empty, loading, total (the center caption), other, inspectHint, slice / value / percent (table headers), figure ((label, total) => name), more ((count) => "+3 more").
classNamesPartial<Record<DonutChartSlot, string>>Classes for plot (the drawing: svg + center), svg, segment, center, centerValue, centerLabel, centerPercent, legend, legendItem, tooltip, table and inspect.
Parts
DEFAULT_DONUT_CHART_LABELS
constantThe English strings the chart uses (table headers Slice, Value and Share, the figure's accessible name, the "+N more" line of the Other tooltip, the keyboard hint). Spread it and override what you translate.
DONUT_CHART_OTHER_ID
constantThe id of the folded "Other" slice ("__other__"). onValueChange reports it when that slice is selected, and you can pass it as value to select it.
DonutChartDatum
typeOne slice: { id, label, value, color?, name? }. The id is what value and onValueChange carry; color takes a palette slot, a tone or any CSS color.
DonutChartLabels
typeThe shape of labels: the chart kit's shared strings plus slice, value, percent, figure and more.
Examples
Pie with status tones
Invoices by status as a pie, colored by tones, with a total row in the legend.
Folded into Other
Ten cities, maxSlices 5: the smallest fold into one gray slice.
Sizes and KPI cards
sm, md and lg, and a small legendless donut inside a KPI card.
Controlled selection
value / onValueChange shared with a list next to the chart.
Custom center
centerValue and centerLabel replace the total.
Loading, empty and phone
Skeletons, the empty state and the stacked phone layout.
Files
- components/ui/donut-chart.tsx
FAQ
Is the React Donut chart component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Donut chart?
Run npx wingo-ui@latest add donut-chart. It also adds the Wingo UI items it builds on (Chart kit, Motion presets, UIProvider, useControllableState and useLocale) and the npm packages motion and d3-shape. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/donut-chart.
How does Donut chart behave on phones?
On a phone, tap a slice to select it (the ring is widened by 12px on each side, so thin rings and slivers are easy to hit), slide a finger along the ring to scrub (the center reads each slice and the one you let go on is selected), tap the hole or outside the ring to clear.
See also
Used in
More React chart and stat components
Articles using this component
- React Charts Tailwind: 4 Charts and a Kit for Any Theme
Component guides10 min read