Donut Chart
- Stable
- WCAG 2.2 evidence
- RTL
A donut chart: the parts of a whole as slices of a ring, with the total in the hole and a legend of
rows that give every slice its value and share. Data driven: slices in, SVG out; the same props render the static SVG on the
server. Accessibility: the figure is named by title, described by summary and backed by a data table with every value and share (WCAG
1.1.1, 1.3.1). The plot is ONE tab stop: any arrow key moves between slices (Right and Down go clockwise), Home and End jump to the
first and last, and each slice is announced with its name, value and share. Hovering or focusing a slice lifts it outward and shows
a tooltip that stays while hovered and closes with Escape (1.4.13, 2.1.1). Slices differ by colour AND fill pattern, and the legend
rows repeat the pattern next to the name, value and share as text (1.4.1). The draw-in and the lift are behind motion-safe:. The
donut is a fixed 240 px and the legend wraps under it on a narrow card (1.4.10); the plot stays left-to-right in right-to-left
pages while the card, legend and table mirror, and digits follow the locale. Consumer duties: a title that asks the question, a summary with the main message, units in valueFormat, meaningful slice names;
a donut is for a handful of parts of a whole, use a BarChart to compare many values precisely.
import { DonutChart } from "@nexera-ui/react";- 15
- examples
- 22
- props
- 4
- live controls
- 1
- platform
- 9
- WCAG criteria
- 1
- block uses it
Try every prop. Copy the code.
Change the props and the code updates. Check light and dark, LTR and RTL, and phone width.
import { DonutChart } from "@nexera-ui/react";
<DonutChart />Examples 14
The same examples as Storybook, rendered live. Open Code to copy one.
With table disclosure
The table is a disclosure under the plot by default, the visible text alternative of the chart.
import { DonutChart } from "@nexera-ui/react";
<DonutChart table={{ caption: "Workforce by employment type", categoryLabel: "Type" }} />;
Thin ring custom centre
A thinner ring (innerRadius) and your own wording for the hole.
import { DonutChart } from "@nexera-ui/react";
<DonutChart
innerRadius={0.82}
centerValue="75%"
centerLabel="full-time"
valueFormat={{ maximumFractionDigits: 0 }}
/>;
Plot only
Without the legend rows and without the hole text: the slices stay named in the tooltip and the table.
import { DonutChart } from "@nexera-ui/react";
<DonutChart isLegendHidden isCenterHidden />;
Currency
Formats and units: currency with the locale, large numbers in the hole shrink to fit.
import { DonutChart } from "@nexera-ui/react";
<DonutChart
title="Payroll by employment type"
description="September 2026"
summary="Full-time staff take 86 percent of the Rs 29.0M payroll."
slices={[
{ name: "Full-time", value: 24_900_000 },
{ name: "Contract", value: 3_100_000 },
{ name: "Part-time", value: 700_000 },
{ name: "Intern", value: 343_000 },
]}
valueFormat={{ style: "currency", currency: "PKR", maximumFractionDigits: 0 }}
locale="en-PK"
centerLabel="total payroll"
/>;
Loading
Loading, empty and error are ChartStates inside the card; the header stays.
import { DonutChart } from "@nexera-ui/react";
<DonutChart state={{ status: "loading" }} />;
Empty
import { DonutChart } from "@nexera-ui/react";
<DonutChart
slices={[]}
state={{
status: "empty",
title: "No employees in this filter",
description: "Clear the department filter.",
}}
/>;
Error state
import { Button, DonutChart } from "@nexera-ui/react";
<DonutChart
state={{
status: "error",
title: "Couldn't load the workforce",
description: "Check your connection and try again.",
action: (
<Button variant="secondary" size="sm">
Try again
</Button>
),
}}
/>;
Single slice
One slice closes into a full ring: 100 percent.
import { DonutChart } from "@nexera-ui/react";
<DonutChart
title="Everyone is full-time"
summary="All 128 employees are full-time."
slices={[{ name: "Full-time", value: 128 }]}
/>;
Zero and missing
A slice of 0 is not drawn but keeps its legend row and table row; a missing value reads "No data".
import { DonutChart } from "@nexera-ui/react";
<DonutChart
slices={[
{ name: "Full-time", value: 96 },
{ name: "Contract", value: 18 },
{ name: "Part-time", value: 0 },
{ name: "Intern", value: null },
]}
/>;
Many slices
Twelve slices cycle the eight colours and patterns; tiny slices read "<1%". Group the tail as "Other" in real use.
import { DonutChart } from "@nexera-ui/react";
<DonutChart
title="Headcount by department"
summary="Engineering is the largest of twelve departments."
slices={[
"Engineering",
"Sales",
"Support",
"Operations",
"Finance",
"People",
"Marketing",
"Legal",
"Design",
"Data",
"Security",
"Facilities",
].map((name, i) => ({ name, value: [40, 22, 18, 14, 10, 8, 7, 5, 4, 3, 2, 0.2][i] ?? 1 }))}
centerLabel="employees"
/>;
Long labels
Long slice names wrap in the legend rows, the tooltip and the table; nothing is cut off.
import { DonutChart } from "@nexera-ui/react";
<DonutChart
slices={[
{
name: "Contractors engaged through an external staffing agency on rolling six-month terms",
value: 18,
},
{
name: "Full-time employees in the Karachi head office and the regional sales offices",
value: 96,
},
{ name: "Interns", value: 6 },
]}
/>;
Pinned styles
Keep a slice's colour and pattern when you filter the list: styleIndex pins it.
import { DonutChart } from "@nexera-ui/react";
<DonutChart
slices={[
{ name: "Contract", value: 18, styleIndex: 1 },
{ name: "Intern", value: 6, styleIndex: 3 },
]}
/>;
Range switch
The header controls: a range switch that swaps the data, and a menu button in the actions slot.
import { useState } from "react";
import { DonutChart, IconButton } from "@nexera-ui/react";
import { LuEllipsis } from "react-icons/lu";
const menu = <IconButton aria-label="More options" variant="ghost" icon={<LuEllipsis />} />;
export function RangeSwitch() {
const [selected, setSelected] = useState("now");
const slices =
selected === "now"
? WORKFORCE
: [
{ name: "Full-time", value: 88 },
{ name: "Contract", value: 22 },
{ name: "Part-time", value: 9 },
{ name: "Intern", value: 3 },
];
return (
<DonutChart
slices={slices}
range={{
"aria-label": "Period",
options: [
{ id: "now", label: "Now" },
{ id: "year", label: "A year ago" },
],
value: selected,
onChange: setSelected,
}}
actions={menu}
/>
);
}
Right to left
Right-to-left page: the card, legend rows and table mirror, the donut keeps its clockwise direction from 12 o'clock, digits follow the locale.
import { DonutChart } from "@nexera-ui/react";
<DonutChart
title="القوى العاملة حسب نوع التوظيف"
description="تُرفع الشريحة عند التمرير"
summary="ثلاثة أرباع الموظفين البالغ عددهم ١٢٨ يعملون بدوام كامل."
slices={[
{ name: "دوام كامل", value: 96 },
{ name: "عقد", value: 18 },
{ name: "دوام جزئي", value: 8 },
{ name: "متدرب", value: 6 },
]}
centerLabel="موظف"
valueLabel="العدد"
shareLabel="النسبة"
table={{ categoryLabel: "النوع", label: "عرض البيانات كجدول" }}
/>;
Props 22
Press "Try it" on a card to load that prop into the playground.
22 props shown
slices*Nexerareadonly DonutSlice[]The slices, in the order they run clockwise from 12 o'clock. Keep to about eight: more cycle the colours and patterns and the smallest slices become thin; group the tail as "Other". Each slice gets its own colour AND fill pattern (WCAG 1.4.1), and its name, value and share are in the legend rows, the tooltip and the table.
innerRadiusNexeranumberRadius of the hole as a share of the outer radius, 0.2 to 0.9. A bigger hole is a thinner ring.
centerValueNexeraReactNodeThe headline in the hole. A string or number shrinks to fit the hole.
centerLabelNexeraReactNodeThe caption under the headline. Translate it.
isCenterHiddenNexerabooleanLeave the hole empty.
valueLabelNexerastringHeader of the value column of the table and label of the value row in the tooltip and the point labels. Translate it.
shareLabelNexerastringHeader of the share column of the table and label of the share row in the tooltip and the point labels. Translate it.
title*NexerastringChart title: the question the chart answers ("Headcount"). Required: it names the figure, the plot and the data table (WCAG 1.1.1).
descriptionNexeraReactNodeSecondary line under the title.
summary*NexerastringOne or two sentences with what the chart shows and its main message ("Headcount grew from 96 to 128 over twelve months"). Required: it describes the figure for assistive technology (WCAG 1.1.1); the data table has the details.
rangeNexeraChartRangePropsRange switch in the header: 3M, 6M, 12M. Pass the data of the chosen range back in.
actionsNexeraReactNodeExtra controls in the header, after the range switch.
kpiNexeraChartKpiPropsKPI row above the plot: the headline number with an optional label or change badge.
stateNexeraChartPlotStateThe state that replaces the plot: loading (skeleton), empty or error. A chart without a value to plot shows the empty state
by itself; pass { status: "empty", title, description, action } to give it your wording.
tableNexeraChartTableOptionsThe data table that is the text alternative: caption, first-column header and whether it is visible or only for assistive technology.
localeNexerastringLocale of digits and separators.
valueFormatNexeraNumberFormatOptionsIntl.NumberFormat options for values ({ style: "currency", currency: "PKR" }, { style: "percent" }). Serialisable, so it
works from a Server Component; formatValue needs a Client Component.
formatValueNexera(value: number) => stringFormats a value for tooltips, point labels and the table (full precision). Wins over valueFormat.
heightNexeranumberHeight of the plot in px. The card grows with the header, KPI and legend around it.
isLegendHiddenNexerabooleanHide the legend. The series stay named in the tooltip, the point labels and the table.
plotLabelNexerastringAccessible name of the plot group. Translate it.
noDataLabelNexerastringText for a missing value in the tooltip and the table. Translate it.
* Required. React Aria props shown are the ones most apps use; the component accepts the rest of its React Aria props too.
Accessible by default.
Built on React Aria, and covered by the WCAG 2.2 evidence generated on every build.
WCAG 2.2 evidence
8 direct · 1 supporting- 1.1.1Non-text ContentLevel A · tested directly
- 1.3.1Info and RelationshipsLevel A · tested directly
- 1.4.1Use of ColorLevel A · tested directly
- 1.4.10ReflowLevel AA · tested directly
- 1.4.13Content on Hover or FocusLevel AA · tested directly
- 2.1.1KeyboardLevel A · tested directly
- 2.3.3Animation from InteractionsLevel AAA · supporting test
- 4.1.2Name, Role, ValueLevel A · tested directly
- 4.1.3Status MessagesLevel AA · tested directly
Fits any width.
Nexera components respond to the space they are given. Drag the corner of the frame, or pick a width.
Styling hooks
Pass className to add Tailwind classes (merged last). State is exposed as data attributes, so you can style it with variants like data-pressed:.
<DonutChart className="shadow-sm" />Used in blocks
Related components
- AccordionItemOne collapsible section of an FAQ or settings page.
- AreaChartA stacked area chart: layers piled up over time with the title, range switch, actions and legend of the card.
- AvailabilityRowOne person's availability across a window of the day: an avatar with initials and the name, and a timeline with the busy periods drawn as bars.
- AvatarA person or organisation shown as a photo, initials or an icon.
- AvatarGroupA row of overlapping avatars with a "+N" chip for the rest.
- AvatarLabelAn avatar with a name and an optional subtitle beside it, for a person in a list, a header or a mention.
- BadgeShort status or category label: `Tone` x `Variant` x `Size`, with an optional dot, leading icon and remove button.
- BadgeGroupA pill that pairs a small badge with a short message, for an announcement or a status line.