DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

Pie Chart

Token-driven pie, donut, ring and half-donut charts built on Recharts v3 — clockwise from twelve, with a centre label slot, geometry-aware slice rounding, percent labels, a controlled legend and a named accessible figure.

Playground

Installation

pnpm add @tessinaui/ui recharts

Usage

import { PieChart } from "@tessinaui/ui";
import type { ChartConfig } from "@tessinaui/ui";

// config is keyed by each slice's nameKey value
const config: ChartConfig = {
  chrome: { label: "Chrome", color: "var(--chart-1)" },
  safari: { label: "Safari", color: "var(--chart-2)" },
  firefox: { label: "Firefox", color: "var(--chart-3)" },
};

const data = [
  { browser: "chrome", visitors: 275 },
  { browser: "safari", visitors: 200 },
  { browser: "firefox", visitors: 187 },
];

<PieChart
  data={data}
  config={config}
  dataKey="visitors"
  nameKey="browser"
  donut
  label="Chrome leads browser share"
  description="Of 662 visitors, 275 use Chrome, 200 Safari and 187 Firefox."
/>

When to use one

A pie answers one question: how does this whole divide into parts? It is weakest at the thing charts are usually for — comparing quantities — because people read angles far less accurately than lengths.

  • Few slices. GitHub Primer says "no more than five slices"; Adobe Spectrum says "fewer than 6 segments". Past that, bucket the tail into an Other slice (see the example below) or switch to a bar chart.
  • Parts of one total. If the values do not sum to a meaningful whole, a pie is the wrong shape — use BarChart.
  • Comparing categories precisely → BarChart. Change over time → LineChart or AreaChart. One value against a target → Meter or Progress. Concentric progress rings → RadialChart.
  • A donut buys you the middle, which is why most real donuts put the total there. Reach for donut whenever you have a number worth stating.

Negative values cannot be drawn as a share of a whole; the component warns in development and the slice would sweep backwards over its neighbours.

Examples

Default

A plain pie. Slices sweep clockwise from twelve o'clock and carry a hairline separator in the chart surface colour, so two similar neighbours stay distinguishable.

Donut with a total

Ring

A thin ring with gaps and rounded caps — the summary-glyph shape.

Gaps and rounded caps

sliceGap opens a real angular gap; rounded caps the ends. The corner radius is resolved against each slice's own geometry, so a narrow slice keeps its rounding instead of silently squaring off.

Half donut

startAngle / endAngle are plain degrees, so a gauge is a range rather than a separate component.

Corner rounding

Percent labels

Labels inside the slice

Value formatting

Interactive legend

Bucketing the tail into "Other"

No charting library surveyed ships small-slice bucketing, so it is a data transform rather than a prop.

Minimal

Accessible figure

States

The skeleton mirrors the chart it stands in for — a ring for a donut, a disc for a pie — and keeps its own loading announcement. An error carries the error intent, so it never reads as neutral chrome the way an empty state does.

Accessibility

  • label is the accessible name, and it is not optional in practice. Recharts makes the chart svg a keyboard-focusable role="application" surface; without label the root carries no role at all, so aria-label on it is ignored and a keyboard user lands on an unnamed application. label renders the chart as a named role="figure".
  • description carries the insight as a visually hidden summary, so a static read needs no interaction. State the numbers a sighted reader gets from the slices.
  • The centre text is decorative (aria-hidden) because it repeats data the chart already carries — put the same number in description.
  • Arrow keys step the tooltip through the slices and Escape dismisses it without moving focus.
  • Colour is never the only encoding: the legend names every slice, and labels can print names or percentages on the chart itself.
  • Chart colours are gated at 3:1 against the background (marks), tick and label text at 4.5:1.

API Reference

PieChart

PropTypeDefaultDescription
dataPieChartDatum[]—One row per slice.
configChartConfig—Slice metadata (label, color), keyed by the nameKey value.
dataKeystring—Datum key holding the numeric value. Warns in development if it is not a key of the first row.
nameKeystring—Datum key holding the slice name.
labelstring—Accessible name; renders the chart as a role="figure".
descriptionstring—Visually hidden summary for assistive tech.
donutbooleanfalseSugar for innerRadius="55%".
innerRadiusnumber | string0Hole size, px or a percentage of the radius. Overrides donut.
outerRadiusnumber | string"80%"Outer radius, px or a percentage.
centerValuestring | number—Big text in the middle of a donut. Ignored without a hole.
centerLabelstring—Caption under centerValue.
rounded"none" | "sm" | "md" | "lg" | "full""none"Slice corner rounding. full is the roundest the geometry allows.
sliceGapnumber0Angular gap between slices, in degrees.
showSliceBorderbooleantrueHairline between adjacent slices, in the chart surface colour.
minSliceAnglenumber0Minimum angle for a non-zero slice, so a tiny share stays visible.
startAnglenumber90Where the first slice starts, in degrees.
endAnglenumber-270Where the last slice ends.
showLabelsbooleanfalsePrint a label per slice.
labelType"value" | "percent" | "name" | "name-percent""value"What each label prints.
labelPosition"outside" | "inside""outside"Outside with a leader line, or inside the slice.
valueFormatter(value, name) => string | number—Formats labels and the tooltip.
sliceColor(datum, index, name) => string | undefined—Per-slice fill override.
showLegendbooleantrueRender the legend.
legendPosition"top" | "bottom""bottom"Legend placement.
hiddenSlicesstring[]—Slice names hidden by the consumer. Legend interactivity is controlled.
onSliceToggle(name: string) => void—Called when a legend item is clicked.
onSliceClick(datum, index, name) => void—Called when a slice is clicked.
showTooltipbooleantrueRender the tooltip.
size"sm" | "md" | "lg""md"Diameter cap: 240 / 320 / 400px.
dir"ltr" | "rtl"—Direction; unset by default so a global dir is respected.
loadingbooleanfalseRender the skeleton.
errorReactNode—Render the error message instead of the chart.
emptyStateReactNode"No data to display"Shown with no rows, or when every value is zero.
childrenReactNode—Extra Recharts elements drawn inside the chart.

Data attributes

AttributeOnValue
data-componentroot"pie-chart"
data-slotroot"chart" (from ChartContainer)
data-slotcentre text"pie-chart-center"

Agent notes

  • rounded only reads as rounded on a donut. With no hole the two straight edges of a slice meet at a point, so only the outer corners can round — this is inherited from d3 and is true of every charting library.
  • A zero-value slice draws nothing but keeps its legend row. Set minSliceAngle to floor tiny non-zero values so they stay visible.
  • An all-zero series renders the empty state, not a blank circle: Recharts needs a positive total.
  • A single 100% slice is a plain circle — Recharts ignores corner radius on a full-circle sector.
  • sliceGap is dropped by Recharts when there is only one slice. A full circle gets one gap per slice; an arc gets one fewer.
  • The legend is the interactive surface (onSliceToggle), not the slice — that is what the field does, and it is reachable by keyboard.

Theming

Slice colours come from config, which normally points at the --chart-1 … --chart-6 tokens; a fill on a datum or a sliceColor callback overrides per slice. The separator between slices paints --chart-surface, the same token the stacked bars use. Centre text uses --foreground and --muted-foreground.

Migration

  • Slices now sweep clockwise from twelve o'clock. Recharts' own default starts at three o'clock and runs counter-clockwise; pass startAngle={0} and endAngle={360} to restore the previous rendering.
  • Adjacent slices now carry a hairline separator. Pass showSliceBorder={false} for the previous gapless look.
  • rounded="full" used to render identically to rounded="none" on a non-donut pie; it now rounds the outer corners.
PickerPopover

On this page

PlaygroundInstallationUsageWhen to use oneExamplesDefaultDonut with a totalRingGaps and rounded capsHalf donutCorner roundingPercent labelsLabels inside the sliceValue formattingInteractive legendBucketing the tail into "Other"MinimalAccessible figureStatesAccessibilityAPI ReferencePieChartData attributesAgent notesThemingMigration