# Donut chart

> Parts of a whole as a ring or a half-ring gauge: point at a slice and the centre rolls to it, hide one and the rest slide round.

A donut is the quickest way to show where something went, and most versions are a picture that redraws from scratch. In this one each slice draws round from where it starts the first time it’s seen, one after another, while the total in the centre rolls up from zero. Point at a slice or its row and it thickens as the others step back; the centre’s label morphs to its name and the figure rolls from the total to its value, with its share opening underneath. Click a row in the legend to take that slice out: the others slide round to close the gap and every share and the total roll to the new sums. New numbers, another week say, slide every slice to its new place. As a half-ring gauge with a max, the slices fill only their part of it, so a budget or a storage quota reads at a glance.

## Install

```bash
npx shadcn@latest add https://hairlineui.com/r/donut-chart.json
```

Needs React 19, Tailwind CSS v4 and shadcn/ui theme variables (any style, light and dark). Icons from lucide-react.

- npm: none
- Hairline UI components it needs (the CLI installs them too): `number-roll`, `text-morph`
- Docs and live preview: https://hairlineui.com/ui/donut-chart
- Registry item (source): https://hairlineui.com/r/donut-chart.json

## Usage

```tsx
import { DonutChart } from "@/components/donut-chart";

<DonutChart
  title="Spend by service"
  data={[
    { id: "compute", label: "Compute", value: 5240 },
    { id: "storage", label: "Storage", value: 3180 },
    { id: "bandwidth", label: "Bandwidth", value: 2260 },
  ]}
  format={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }}
/>
```

## A budget from your own numbers

Sum spending by category on the server and pass the budget as max; the gauge does the rest.

```tsx
import { DonutChart } from "@/components/donut-chart";
import { db } from "@/lib/db";

export async function BudgetGauge({ month }: { month: string }) {
  const rows = await db.spendByCategory({ month }); // [{ category: "Ingredients", total: 4820 }, …]
  const budget = await db.budgetFor({ month }); // 15000

  return (
    <DonutChart
      variant="gauge"
      title="October budget"
      centerLabel="Spent"
      max={budget}
      data={rows.map((r) => ({ id: r.category, label: r.category, value: r.total }))}
      format={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }}
    />
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `data` | `DonutSlice[]` | – | The slices in order: { id, label, value, color? }. |
| `variant` | `"donut" \| "gauge"` | `"donut"` | A full ring, or a half ring read from left to right. |
| `max` | `number` | – | What the whole ring stands for. Slices fill total ÷ max of it and the centre says “of” the max. |
| `title / description / action` | `ReactNode` | – | The header: a title, a line under it and something beside it, such as a period switch. |
| `centerLabel` | `string` | `"Total"` | Above the figure while no slice is pointed at. |
| `format` | `Intl.NumberFormatOptions` | – | How figures are written, e.g. { style: "currency", currency: "USD" }. |
| `locales` | `string \| string[]` | – | For number formatting. |
| `thickness` | `number` | `16` | Ring thickness in px at full size. |
| `hiddenSlices / defaultHiddenSlices` | `string[]` | – | Ids of the slices taken out; controlled or not. |
| `onHiddenSlicesChange` | `(ids: string[]) => void` | – | Called when the legend hides or shows a slice. |
| `legend` | `boolean` | `true` | Show the legend, which also hides and shows slices. |

## States

- **arrive**: In view for the first time, each slice draws from its start (900ms, 90ms apart) and the total rolls up from zero.
- **point**: Pointing at a slice or its row thickens it by 5px and dims the rest to 30%; the centre label morphs to its name, the figure rolls to its value and its share opens underneath. On touch a tap holds a slice and a tap elsewhere lets go.
- **toggle**: Clicking a legend row hides its slice (the last one shown can’t be hidden): it shrinks into its neighbours as they slide round, and the shares and total roll to the new sums.
- **data**: New values move every slice to its new length and place together (900ms) and roll every figure.
- **gauge**: variant="gauge" lays the slices along a half ring from the left; with max, they fill only total ÷ max of it and the centre says “of” the max.

## What it handles

Donuts usually come from a chart library that redraws on every change, or from SVG arc maths written by hand. This takes plain slices and handles the drawing, the gaps, the centre readout and the legend.

- **Ring or gauge**: A full ring, or a half ring read from left to right, from the same slices.
- **A centre that answers**: The total sits in the middle; point at a slice and it rolls to that slice’s value and share.
- **A legend that filters**: Each row shows a slice’s figure and share, and clicking it takes the slice out while the rest close the gap.
- **Changes, not redraws**: New numbers slide each slice to its new place and roll every figure.
- **Against a max**: Give it a budget or a quota and the slices fill only their part, the rest of the track left empty.
- **Your theme’s colours**: Slices take --chart-1 to --chart-5 in order, or any colour you pass.

## Replaces

a chart library, SVG arc maths, a legend with toggles, a centre label, number formatting

## Where it fits

- **Spend and billing pages**: Cloud or ad spend by service or channel, with the total in the middle.
- **Sales dashboards**: Revenue by product, plan or region, switching between periods.
- **Budgets and quotas**: As a gauge with a max: money spent against a budget, storage used against a plan.
- **Survey and poll results**: How answers split, with a legend that can take an option out to compare the rest.

## Notes

- Grey in a new shadcn project? Its default theme sets --chart-1 to --chart-5 to greys. Set five colours, light and dark, as in Theming › Chart colours (/ui/theming#charts).
- The legend is the accessible part: each row is a button (aria-pressed) whose label gives the slice’s name, value and share; the ring is decoration.
- Focusing a legend row points at its slice, so keyboard users get the same centre readout.
- Colours are --chart-1 to --chart-5 from your theme, in order; past five they repeat a step darker. Pass color on a slice to set your own.
- Each slice is one circle’s stroke, so every change is a CSS transition of its length and start; no chart library and no animation frame loop.
- It sizes to its own width: from about 450px the type steps up, and from about 510px the ring’s legend moves beside it (the gauge keeps it underneath).
- Installs Number roll and Text morph. With reduced motion slices change in place.

License: MIT (https://hairlineui.com/license#components)
