Donut chart
A donut or pie with the total in the center and a legend with values and shares. Hovering a segment or legend row shows that part in the center.
npx prodular-ui add donut-chartParts of a total
Total in the center, legend on the right with each part's share. Colors follow chart-1 to chart-5.
"use client"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"import { DonutChart } from "@/components/ui/donut-chart"
const seats = [ { name: "Enterprise", value: 1840 }, { name: "Team", value: 1215 }, { name: "Pro", value: 640 }, { name: "Starter", value: 305 },]
export default function DonutChartPlans() { return ( <Card className="w-full max-w-lg"> <CardHeader> <CardTitle>Seats by plan</CardTitle> <CardDescription>Paid seats across 312 workspaces</CardDescription> </CardHeader> <CardContent> <DonutChart data={seats} centerLabel="Seats" /> </CardContent> </Card> )}Pick a segment
onValueChange with activeKey turns segments and legend rows into a filter. Click again to clear.
"use client"
import * as React from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"import { DonutChart, type DonutChartItem } from "@/components/ui/donut-chart"
const spend: DonutChartItem[] = [ { key: "cloud", name: "Cloud infrastructure", value: 128400 }, { key: "payroll", name: "Contractors", value: 61900 }, { key: "software", name: "Software", value: 38100 }, { key: "travel", name: "Travel", value: 12750 },]
const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", notation: "compact", maximumFractionDigits: 1 })
export default function DonutChartSpend() { const [active, setActive] = React.useState<string | null>(null) const selected = spend.find((s) => s.key === active) return ( <Card className="w-full max-w-xs"> <CardHeader> <CardTitle>Q3 vendor spend</CardTitle> <CardDescription>{selected ? `Filtered to ${selected.name.toLowerCase()}` : "Pick a category to filter the report"}</CardDescription> </CardHeader> <CardContent> <DonutChart data={spend} legend="bottom" size={168} thickness={18} valueFormatter={(v) => usd.format(v)} activeKey={active} onValueChange={(item) => setActive(item?.key ?? null)} /> </CardContent> </Card> )}Pie, loading and empty
variant="pie" for two or three parts, a skeleton ring while loading, a muted ring when there is nothing to show.
Pie, two parts
- Paid on time84.9%214
- Paid late15.1%38
Paid on time: 214, Paid late: 38
Loading
Empty
"use client"
import { DonutChart } from "@/components/ui/donut-chart"
export default function DonutChartStates() { return ( <div className="grid w-full max-w-3xl gap-10 md:grid-cols-3"> <div className="flex flex-col gap-4"> <p className="text-[13px] font-medium text-muted-foreground">Pie, two parts</p> <DonutChart variant="pie" size={120} legend="bottom" data={[ { name: "Paid on time", value: 214 }, { name: "Paid late", value: 38 }, ]} /> </div> <div className="flex flex-col gap-4"> <p className="text-[13px] font-medium text-muted-foreground">Loading</p> <DonutChart data={[]} loading size={120} thickness={16} legend="bottom" /> </div> <div className="flex flex-col gap-4"> <p className="text-[13px] font-medium text-muted-foreground">Empty</p> <DonutChart data={[]} size={120} thickness={16} legend="none" empty="No invoices" /> </div> </div> )}Anatomy
<DonutChart data={[{ name: "Enterprise", value: 1840 }, { name: "Team", value: 1215 }]} centerLabel="Seats" legend="right"/>Guidance
Use it when
- Parts of one total with five or fewer parts: seats by plan, spend by category, tickets by status.
- The center for the total, so the chart answers how much and how it splits at once.
- onValueChange when choosing a part should filter a table or report next to it.
Avoid
- More than five parts. Group the smallest into Other, or use Bar list.
- Comparing parts across periods. Use a stacked bar chart.
- variant="pie" for more than three parts, since angles are hard to compare.
Rules for AI agents
Published in llms-full.txt, the registry and the MCP server, so coding agents follow them when they build with this component.
- Pass data as { name, value }[]. Leave color out to get chart-1 to chart-5 in order; set color only to a var(--chart-N) token.
- Format the center and legend values with valueFormatter, for example compact currency.
- Use legend="bottom" in narrow cards and legend="right" from about 420px wide.
- Set centerLabel to the unit (Seats, Invoices). centerValue overrides the total, for example 62% used.
- Show loading while data is fetched and pass empty with what is missing, like No invoices.
API
DonutChart
| Prop | Type | Default | Description |
|---|---|---|---|
| data | { name, value, key?, color? }[] | Segments in drawing order, clockwise from the top. | |
| variant | "donut" | "pie" | "donut" | Pie fills the center and drops the center text. |
| size | number | 176 | Diameter in pixels. |
| thickness | number | 22 | Ring width in pixels. |
| valueFormatter | (value: number) => ReactNode | Formats the center and legend values. | |
| centerLabel / centerValue | ReactNode | "Total" / the total | Center text when nothing is hovered. |
| legend | "right" | "bottom" | "none" | "right" | Legend placement. Right stacks under the chart on small screens. |
| activeKey / onValueChange | string | null / (item | null) => void | Makes segments and legend rows selectable. | |
| loading | boolean | false | Shows a skeleton ring and legend. |
| empty | ReactNode | "No data" | Center text when the total is zero. |
Installation
The Prodular UI CLI adds the source to components/ui with everything it depends on. In a project that already uses shadcn/ui, the shadcn CLI installs it from the Prodular registry too. See Installation for the first setup.
npx prodular-ui add donut-chart# or, with the registry in components.jsonnpx shadcn@latest add @prodular/donut-chart