const data = /* ... */;
const labels = /* ... */;
<PieChart
data={data}
value="visitors"
name="browser"
labels={labels}
innerRadius={0.55}
stroke="var(--color-bg)"
strokeWidth={2}
ariaLabel="Visitors by browser"
/>A pie chart answers one question: how does this total break down? It reads well with a handful of slices and a clear leader, and badly with a dozen near-equal ones — comparing angles is harder than comparing lengths. When the ranking matters more than the share, use a bar chart.
PieChart takes your rows and the names of the fields to read. Colors, typography, tooltip, and legend all come from your design system.
Installation
pnpm dlx shadcn@latest add @dotui/chart-pieIt brings the chart core — the host, the palette, and the shared frame — along with it.
Usage
import { PieChart } from "@/components/ui/chart-pie"One row per slice. value names the field holding the magnitude, name the field holding the slice key. A chart is a figure, so ariaLabel is required.
const data = [
{ browser: "chrome", visitors: 275 },
{ browser: "safari", visitors: 200 },
{ browser: "firefox", visitors: 187 },
]
export function Example() {
return (
<PieChart
data={data}
value="visitors"
name="browser"
labels={{ chrome: "Chrome", safari: "Safari", firefox: "Firefox" }}
ariaLabel="Visitors by browser"
/>
)
}data is compared by identity, so define it outside your component — or memoize it. Every other prop is a flat scalar and can change freely.
Slices are drawn in data order and take colors from --chart-1 through --chart-8 in the same order. Set seriesOrder to lead the color slots and the legend — slices the data carries but seriesOrder omits follow in the order they first appear.
Geometry
Radii are ratios, never pixels, so a pie keeps its proportions at every size.
innerRadiusabove0makes it a donut.0.55is a good starting point.outerRadiusshrinks the ring inside the available radius;radiusRatioreserves room around the whole chart for labels and the legend.startAngleandendAngleare radians clockwise from twelve o'clock.endAngle={Math.PI}draws a semicircle.padAngleopens a gap between slices,cornerRadiusrounds their corners.
Slices touch by default. To separate them the way a printed chart does, stroke them with the page background:
<PieChart
data={data}
value="visitors"
name="browser"
innerRadius={0.55}
stroke="var(--color-bg)"
strokeWidth={2}
ariaLabel="Visitors by browser"
/>activeIndex pushes one slice out of the ring by activeOffset, for calling out the slice the surrounding copy is about.
Labels and center content
sliceLabel draws text on each slice — 'value' for the magnitude, 'name' for the key — positioned with sliceLabelRadius and styled with sliceLabelFill and sliceLabelFontSize. Slice labels are decorative: they paint on the surface but never become keyboard stops or tooltip targets — the arc beneath them carries the interaction.
The hole in a donut is best filled with plain HTML: anything passed as children renders as an overlay above the surface and ignores pointer events.
<PieChart
data={data}
value="visitors"
name="browser"
innerRadius={0.6}
ariaLabel="Visitors by browser"
>
<div className="flex h-full flex-col items-center justify-center">
<span className="text-3xl font-bold">{total}</span>
<span className="text-sm text-fg-muted">Visitors</span>
</div>
</PieChart>Concentric rings
A second series is a second ring, not a stack. pieRing builds one, and polarMarks splices it inside the chart's polar container — plain marks would land outside the transform. Build it outside your component so it keeps its identity across renders.
import { pieRing, PieChart } from "@/components/ui/chart-pie"
const mobileRing = pieRing({
id: "mobile",
data: mobile,
value: "mobile",
name: "month",
innerRadius: 0.7,
outerRadius: 0.95,
})<PieChart
data={desktop}
value="desktop"
name="month"
outerRadius={0.6}
polarMarks={mobileRing}
ariaLabel="Desktop and mobile visitors by month"
/>Both rings key their colors off the same field, so one month is one color from the middle out.
Animation
Slices sweep in through their angles on first client paint, one after another, and spring between data states — server-rendered charts replay the entrance once hydration lands. animate takes false for an immediate repaint, or a tween or spring configuration to change the feel.
Accessibility
ariaLabelis required and names the figure; addariaDescriptionwhen the takeaway needs a sentence.- The chart surface is in the tab order. Arrow keys move between slices,
HomeandEndjump to the first and last,Enterand Space pin the tooltip, andEscapedismisses it. - Color alone never carries the distinction — the legend, tooltip, and slice labels all name the slice.
tooltip={false}keeps the focus stops but removes their live region, so screen readers land on silent points. Turn it off only for a decorative chart.
Examples
Default
Donut
Active Slice
Donut with Text
Value Labels
Custom Labels
Name Labels
Legend
No Separator
Stacked Rings
API Reference
PieChart
Pie and donut chart. One row per slice: `value` names the field holding the magnitude, `name` the field holding the slice key. Radii are ratios of the chart's resolved radius, not pixels, so a pie keeps its proportions at every size.
| Prop | Type | Default | |
|---|---|---|---|
readonly unknown[] | — | ||
readonly string[] | — | ||
Readonly<Record<string, string>> | — | ||
number | 0 | ||
number | 1 | ||
number | 0.9 | ||
number | — | ||
number | 0 | ||
number | 2 * Math.PI | ||
number | 0 | ||
number | 0 | ||
string | — | ||
number | — | ||
number | — | ||
number | 0.08 | ||
"name" | "none" | "value" | "none" | ||
number | — | ||
string | "var(--color-fg)" | ||
number | 12 | ||
boolean | false | ||
readonly PolarMarkLayer[] | — | ||
union | "nearest" | ||
'point' | 'pointer' | 'group-center' | "point" | ||
string | — | ||
string | — | ||
number | — | ||
boolean | true | ||
boolean | true | ||
ChartAnimate | { type: "spring", stiffness: 170, damping: 26 } | ||
number | 256 | ||
number | — | ||
number | — | ||
number | — | ||
string | — | ||
ReactNode | function | — | ||
ReactNode | — | ||
Last updated on 9/7/2026