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
npx 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 '@/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 fix that assignment when the data can arrive sorted differently.
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 part of the chart surface, so they are also focusable points; they carry the same slice as the arc under them, so the tooltip reads the same either way.
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 '@/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
Pie charts do not animate by default. Arc paths animate by interpolating their d attribute, and an arc that crosses half a turn flips an SVG flag mid-tween — which briefly draws an invalid path. Pass animate explicitly if your slices never cross that boundary.
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.
Examples
Default
Donut
Active Slice
Donut with Text
Value Labels
Custom Labels
Name Labels
Legend
No Separator
Concentric 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. Interaction and animation props are shared by every family — see `ChartBehaviorProps` — and the host props (`height`, `className`, callbacks) live on `Chart`.
| Prop | Type | Default | |
|---|---|---|---|
readonly unknown[] | — | ||
Readonly<Record<string, 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 string[] | — | ||
readonly unknown[] | — | ||
ReactNode | — | ||
Interaction and animation props are shared by every chart family:
Interaction and animation props shared by every chart family component. They are flat scalars on purpose: the chart definition is memoized on a serialized key, and a nested option object would silently go stale.
| Prop | Type | Default | |
|---|---|---|---|
ChartFocus | "group-x" | ||
number | — | ||
ChartTooltipAnchor | "group-center" | ||
boolean | true | ||
false | — | ||
ChartAnimate | { duration: 240, respectReducedMotion: true } |
The host props — height, width, className, and the focus callbacks — are documented on the Chart page.
Last updated on 8/1/2026