Skip to content

Charts

Pie Chart

A parts-of-a-whole comparison paired with a persistent legend and exact-value table.

When to use it: Use it for a few non-negative categories whose total forms a meaningful whole.

On this page

Example

Loading example…
Pie Chart.tsxtsx
import {
  ChartDescription,
  ChartLegend,
  ChartTable,
  ChartTitle,
  ChartTooltip,
  PieChart,
  PieChartPlot,
  PieChartSlice,
} from "@comp0/react";

const traffic = [
  { label: "Direct", value: 4200 },
  { label: "Search", value: 3100 },
  { label: "Referrals", value: 1800 },
  { label: "Social", value: 900 },
] as const;

const colors = [
  "bg-teal-600 fill-teal-600 dark:bg-teal-400 dark:fill-teal-400",
  "bg-sky-600 fill-sky-600 dark:bg-sky-400 dark:fill-sky-400",
  "bg-violet-600 fill-violet-600 dark:bg-violet-400 dark:fill-violet-400",
  "bg-amber-500 fill-amber-500 dark:bg-amber-400 dark:fill-amber-400",
] as const;

const formatVisits = (value: number) => value.toLocaleString("en-US");

export function Example() {
  return (
    <PieChart
      values={traffic}
      categoryLabel="Source"
      valueLabel="Visits"
      formatValue={formatVisits}
      className="w-full max-w-2xl rounded-lg has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-4 has-[:focus-visible]:outline-teal-600 dark:has-[:focus-visible]:outline-teal-400"
    >
      <ChartTitle className="text-base font-semibold text-zinc-950 dark:text-zinc-50">
        Traffic by source
      </ChartTitle>
      <PieChartPlot
        aria-label="Pie chart showing traffic share by source"
        className="mx-auto mt-5 aspect-square w-full max-w-xs overflow-visible"
      >
        {(slice) => (
          <PieChartSlice slice={slice} className="group outline-none">
            <path d={slice.path} className={colors[slice.index]} />
          </PieChartSlice>
        )}
      </PieChartPlot>
      <ChartLegend className="mt-5 grid gap-2 text-sm [&>li]:grid [&>li]:grid-cols-[auto_1fr_auto] [&>li]:items-center [&>li]:gap-2">
        {({ formattedValue, index, percentage, value }) => (
          <>
            <span aria-hidden="true" className={`size-3 rounded-sm ${colors[index]}`} />
            <span>{value.label}</span>
            <span>
              {formattedValue} · {percentage.toFixed(0)}%
            </span>
          </>
        )}
      </ChartLegend>
      <ChartDescription className="mt-5 text-sm text-zinc-600 dark:text-zinc-400">
        Direct visits are the largest source, while social accounts for less than one tenth.
      </ChartDescription>
      <ChartTable className="mt-4 w-full border-collapse text-left text-sm [&_td]:border-t [&_td]:border-zinc-200 [&_td]:py-2 [&_th]:border-zinc-200 [&_th]:py-2 dark:[&_td]:border-zinc-800 dark:[&_th]:border-zinc-800">
        <caption className="sr-only">Traffic source values</caption>
        <thead>
          <tr>
            <th scope="col">Source</th>
            <th scope="col">Visits</th>
          </tr>
        </thead>
        <tbody>
          {traffic.map((source) => (
            <tr key={source.label}>
              <th scope="row">{source.label}</th>
              <td>{formatVisits(source.value)}</td>
            </tr>
          ))}
        </tbody>
      </ChartTable>
      <ChartTooltip className="pointer-events-none z-50 rounded-md bg-zinc-950 px-2 py-1 text-sm text-white shadow-lg dark:bg-zinc-50 dark:text-zinc-950" />
    </PieChart>
  );
}

Anatomy

Dashed frames are invisible state providers; shaded shapes own real DOM. Numbered pins match the list below.

A wireframe sketch of the assembled component. Each numbered marker matches a part in the list that follows.

  1. PieChart

    Native figure sharing non-negative categories, labels, and formatting with every part. Owns a DOM element.

  2. ChartTitle

    Native figcaption that visibly names the figure. Owns a DOM element.

  3. PieChartPlot / PieChartSlice

    SVG slices showing each category as a proportion of the whole. Owns a DOM element.

  4. ChartLegend

    Persistent native list pairing every category with its formatted value and percentage. Owns a DOM element.

  5. ChartDescription

    Visible prose summarizing the important proportion. Owns a DOM element.

  6. ChartTable

    Native table composed with an explicit caption, headers, and rows from the chart values. Owns a DOM element.

  7. ChartTooltipOptional

    Optional floating value label shown on hover or focus. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Start PieChart with labelled values plus headings for categories and values.

  2. 2

    Add the supporting parts

    Add ChartTitle, PieChartPlot, and ChartLegend; wrap each rendered path in PieChartSlice so one slice is tabbable and arrow keys reveal the rest.

  3. 3

    Make the behavior clear

    Add ChartDescription and ChartTable for persistent context and exact values; optionally add ChartTooltip for pointer and keyboard details.

    Exampletsx
    <PieChart values={traffic} categoryLabel="Source" valueLabel="Share">
      <ChartTitle>Traffic by source</ChartTitle>
      <PieChartPlot aria-label="Pie chart showing traffic share by source">
        {(slice) => (
          <PieChartSlice slice={slice}>
            <path d={slice.path} />
          </PieChartSlice>
        )}
      </PieChartPlot>
      <ChartLegend />
      <ChartDescription>Direct visits are the largest source.</ChartDescription>
      <ChartTable>
        <caption>Traffic source values</caption>
        <thead>
          <tr>
            <th scope="col">Source</th>
            <th scope="col">Share</th>
          </tr>
        </thead>
        <tbody>
          {traffic.map((source) => (
            <tr key={source.label}>
              <th scope="row">{source.label}</th>
              <td>{source.value}%</td>
            </tr>
          ))}
        </tbody>
      </ChartTable>
      <ChartTooltip />
    </PieChart>;

Keyboard

Enters the chart at its current slice and leaves with one more Tab.
Moves to the next slice, wrapping from the last slice to the first.
Moves to the previous slice, wrapping from the first slice to the last.
Home
Moves to the first slice.
End
Moves to the last slice.
Esc
Dismisses an open ChartTooltip.

Forms and accessibility

Charts are descriptive content and do not create form values.

Accessibility checklist

  • Give PieChartPlot a concise aria-label that identifies the chart type and whole being divided.
  • Keep ChartLegend persistent and adjacent to the graphic so labels and values never depend on hover or focus.
  • Wrap custom paths in PieChartSlice to expose one roving tab stop; each slice receives its category, formatted value, and percentage as a name.
  • ChartTooltip is an optional visual enhancement for hover and focus; never make it the only source of a value.
  • Include ChartTable when exact values matter; its native headers preserve relationships during screen-reader navigation.
  • Do not use color as the only way to distinguish slices; pair every swatch with its visible category label.

API reference

Importtsx
import { ChartDescription, ChartLegend, ChartTable, ChartTitle, ChartTooltip, PieChart, PieChartPlot, PieChartSlice } from "@comp0/react";

PieChart

DOM element

Native figure sharing non-negative categories, labels, and formatting with every part.

PropTypeDescription
valuesreadonly CategoricalChartValue[]Non-negative category values whose total must be positive.
categoryLabelstringVisible headings for legend and table values.
valueLabelstringVisible headings for legend and table values.
formatValue(value: number) => stringFormatter shared by legend and table values.

ChartTitle

DOM element

Native figcaption that visibly names the figure.

PieChartPlot / PieChartSlice

DOM element

SVG slices showing each category as a proportion of the whole.

PropTypeDescription
aria-labelstringConcise text alternative naming the graphic and whole.
children(slice: PieChartSliceState) => ReactNodeCustom slice renderer receiving the category, path, angles, and percentage.
slicePieChartSliceStateSlice state passed from the plot to PieChartSlice.
PieChartSlice childrenReactNodeSVG shapes grouped into one named, keyboard-reachable slice.

ChartLegend

DOM element

Persistent native list pairing every category with its formatted value and percentage.

PropTypeDescription
children(item: ChartLegendItem) => ReactNodeCustom legend entry renderer receiving category, formatted value, and percentage.

ChartDescription

DOM element

Visible prose summarizing the important proportion.

ChartTable

DOM element

Native table composed with an explicit caption, headers, and rows from the chart values.

PropTypeDescription
childrenReactNodeNative caption, thead, tbody, and optional tfoot markup.

ChartTooltip

OptionalDOM element

Optional floating value label shown on hover or focus.

PropTypeDescription
placementPopoverPlacementSide of the active mark; defaults to "top".
offsetnumberDistance from the active mark; defaults to eight pixels.
childrenReactNode | (details: ChartValueDetails) => ReactNodeCustom content receiving the active slice's formatted details.

Style hooks

Attributes that appear while a state is true. Target them with Tailwind data variants such as data-open:bg-zinc-100, or with any CSS selector.

PieChartPlot / PieChartSlice

Style hookMeaning
[data-value]The numeric value represented by this slice or legend entry.
[data-active]The slice currently reached by pointer or keyboard.

ChartLegend

Style hookMeaning
[data-value]The numeric value represented by this slice or legend entry.

ChartTooltip

Style hookMeaning
[data-open]A value tooltip is visible.

Keep exploring