Installation
pnpm dlx shadcn@latest add https://dithercn.dev/r/donut-chart.jsonUsage
import { DonutChart } from "@/components/dithercn/donut-chart";
const data = [
{ label: "North America", value: 48210 },
{ label: "Europe", value: 31870 },
{ label: "Asia Pacific", value: 17430 },
];
export function Regions() {
return <DonutChart data={data} label="Requests today" />;
}Slices run clockwise from 12 o'clock in the order you pass them. Each one draws in the next palette colour (ink, orange, green, pink, purple, blue, red, grey) unless you set colors for the chart or color on the slice.
Every slice is a block of whole dither cells with a solid rim along the outside. fill="gradient" thins out from that rim toward the centre; dotted, hatched and solid fill evenly. The gaps between slices are a constant width in cells, so they stay crisp in both modes.
Hover a slice or its legend row to spotlight it: the others fade, the slice steps out by two cells, and the centre switches from the total to that slice's value and share. Click to keep it lit and press Escape to let go. Focus the chart with Tab and the arrow keys step through the slices.
Examples
Storage card
A smaller ring with fill="hatched". The free space is its own slice with color: "grey" and fill: "dotted", so it reads as empty without a second chart.
Pie in a landing section
mode="pie" drops the hole. The spotlit slice gets a readout card that glides between slices. bloom="high" adds a soft glow in each slice's colour.
Props
| Prop | Type | Default |
|---|---|---|
| data* { label: string; value: number; color?: DitherColor; fill?: FillVariant }, drawn clockwise from the top. | Type: DonutSlice[] | Default – |
| label* What the total counts. Shown in the centre and used in the chart's summary. | Type: string | Default – |
| mode A ring with the total in the middle, or full wedges with a readout over the active slice. | Type: "donut" | "pie" | Default "donut" |
| fill Texture of every slice. A slice's own fill wins. | Type: "gradient" | "dotted" | "hatched" | "solid" | Default "gradient" |
| bloom Soft glow in each slice's colour behind it. | Type: "off" | "low" | "high" | Default "off" |
| colors Colours by slice position, as palette names or CSS colours. A slice's own color wins. | Type: DitherColor[] | Default – |
| size Diameter in CSS pixels. | Type: number | Default 220 |
| thickness Donut only: ring thickness as a share of the radius. | Type: number | Default 0.36 |
| gap Gap between slices, in dither cells. | Type: number | Default 1 |
| unit Shown after every value, such as " GB". | Type: string | Default "" |
| format How values and the total are written. | Type: Intl.NumberFormatOptions | ((value: number) => string) | Default { maximumFractionDigits: 0 } |
| legend Show the legend. When false it stays available to screen readers. | Type: boolean | Default true |
| interactive Hover, keyboard focus and the legend spotlight. false draws a static image. | Type: boolean | Default true |
| animate Sweep the slices in clockwise the first time the chart is seen. | Type: boolean | Default true |
| matrix Threshold pattern of the gradient fill. | Type: "bayer2" | "bayer4" | "bayer8" | "blue-noise" | Default "bayer4" |
| cell Size of one dither cell in CSS pixels. | Type: number | Default --dither-cell |
Notes
- The chart is a focusable group with a summary label. Arrow keys, Home and End move between slices, Enter keeps one lit, and a live region reads the active slice. A hidden table lists every value and share.
- The sweep plays once when the chart scrolls into view. With reduced motion turned on, slices appear at once and the spotlight has no transition.
- The drawing is one SVG path per slice, built from whole cells snapped to device pixels. Hovering only changes opacity and a transform.