Pie Chart

A circle divided into slices, for parts of a single whole.

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-pie

It 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.

  • innerRadius above 0 makes it a donut. 0.55 is a good starting point.
  • outerRadius shrinks the ring inside the available radius; radiusRatio reserves room around the whole chart for labels and the legend.
  • startAngle and endAngle are radians clockwise from twelve o'clock. endAngle={Math.PI} draws a semicircle.
  • padAngle opens a gap between slices, cornerRadius rounds 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

  • ariaLabel is required and names the figure; add ariaDescription when the takeaway needs a sentence.
  • The chart surface is in the tab order. Arrow keys move between slices, Home and End jump to the first and last, Enter and Space pin the tooltip, and Escape dismisses 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

925Visitors

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.

PropType
readonly unknown[]
readonly string[]
Readonly<Record<string, string>>
number
number
number
number
number
number
number
number
string
number
number
number
"name" | "none" | "value"
number
string
number
boolean
readonly PolarMarkLayer[]
union
'point' | 'pointer' | 'group-center'
string
string
number
boolean
boolean
ChartAnimate
number
number
number
number
string
ReactNode | function
ReactNode

Last updated on 9/7/2026