Accessible Pie and Donut Charts
Permalink to "Accessible Pie and Donut Charts"Pie and donut charts are everywhere on dashboards — share of revenue by region, tickets by status, storage by file type. They encode a single message well: how a whole divides into parts. They also rely heavily on colour (each slice a hue matched to a legend) and on angle judgments that are hard for everyone. For screen reader users, the default output of most chart libraries is an image or a set of unnamed paths. Accessible pies are not hard: label slices directly, name them in full, add a table, and keep the slice count small.
This guide belongs to chart type-specific patterns.
Spec reference
Permalink to "Spec reference"- SC 1.1.1 Non-text Content (A) — the chart needs a text alternative: a summary sentence plus data (table).
- SC 1.4.1 Use of Color (A) — a colour legend alone fails; direct labels, patterns or both are needed.
- SC 1.4.11 Non-text Contrast (AA) — slices must be distinguishable from adjacent slices and the background at 3:1, or separated by a contrasting border.
- SC 2.1.1 Keyboard (A) — if slices are interactive (hover details, click to filter), they must be keyboard operable.
- SC 1.3.1 Info and Relationships (A) — the centre text of a donut (“Total 1,204”) should be real text, not part of an image.
When to use — and when not to
Permalink to "When to use — and when not to"Use a pie or donut for two to six parts of a whole where the message is “this part is the majority” or “these two are about equal”. Use a bar chart when users need to compare similar values or more than six categories — bars are easier to compare for everyone.
Do not use a pie for values that do not sum to a meaningful whole, for changes over time, or with a legend as the only labelling. The misapplication: a donut with twelve slices in shades of one brand colour and a legend below; most users, not only colour-blind ones, cannot match slices to labels.
Annotated code example
Permalink to "Annotated code example"<figure aria-labelledby="pie-t" aria-describedby="pie-s">
<h3 id="pie-t">Support tickets by status</h3>
<p id="pie-s">1,204 tickets. Open is the largest share at 34%; Waiting is the smallest at 16%.</p>
<svg viewBox="0 0 320 320" role="group" aria-label="Tickets by status, donut chart">
<!-- Each slice focusable with a full name (SC 2.1.1, 1.1.1) -->
<path tabindex="0" role="img" aria-label="Open: 412 tickets, 34 percent" d="…" fill="#219150" stroke="#ffffff" stroke-width="2"/>
<defs><pattern id="dots" width="6" height="6" patternUnits="userSpaceOnUse"><circle cx="3" cy="3" r="1.2"/></pattern></defs>
<path tabindex="-1" role="img" aria-label="In progress: 305 tickets, 25 percent" d="…" fill="url(#dots)" stroke="#ffffff" stroke-width="2"/>
<!-- Direct labels (SC 1.4.1) -->
<text x="…" y="…">Open 34%</text>
<!-- Centre total as text (SC 1.3.1) -->
<text x="160" y="165" text-anchor="middle" font-weight="700">1,204</text>
</svg>
<table class="chart-table"><caption>Tickets by status</caption>…</table>
</figure>
// One tab stop; arrows move between slices (fewer tab stops for keyboard users)
const slices = [...svg.querySelectorAll('path[role="img"]')];
svg.addEventListener('keydown', (e) => {
const i = slices.indexOf(document.activeElement); if (i < 0) return;
const step = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[e.key]; if (!step) return;
e.preventDefault();
const n = slices[(i + step + slices.length) % slices.length];
slices[i].tabIndex = -1; n.tabIndex = 0; n.focus();
});
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Event | Expected announcement | AT deviations |
|---|---|---|
| Reach the figure | Heading, summary sentence | — |
| Tab to the chart | “Open: 412 tickets, 34 percent” | Some readers prefix “graphic” |
| Arrow right | “In progress: 305 tickets, 25 percent” | — |
| Reach the table | “Tickets by status, table, 5 rows” | — |
| Forced colours | Slices separated by borders and patterns | Fills may become system colours |
Integration context
Permalink to "Integration context"Colour, patterns and contrast for slices follow chart colour & contrast, especially direct labelling instead of legends. Keyboard handling mirrors adding keyboard navigation to canvas bar charts. The table toggle is covered in show-as-table toggles for charts.
Gotchas
Permalink to "Gotchas"Percentages that do not sum to 100. Rounding can produce 99% or 101%. Say “percentages are rounded” in the table caption, or use a largest-remainder rounding method.
Tiny slices. Slices under about 3% are impossible to see or target. Group them into “Other” with the breakdown in the table.
Clockwise order. Start at 12 o’clock and go clockwise, largest first, so the focus order matches visual order.
Common mistakes
Permalink to "Common mistakes"- Legend-only labelling.
- Twelve slices in one hue family.
- Centre totals baked into an image.
- Slices that respond to hover but not focus.
Framework notes
Permalink to "Framework notes"Chart.js pie and doughnut charts render to canvas; add a table and summary, and use the chartjs-plugin-datalabels plugin for direct labels. Highcharts pies with the accessibility module get keyboard navigation and point descriptions automatically — configure accessibility.point.valueDescriptionFormat to include percentages. Recharts Pie renders SVG; add tabIndex and aria-label through custom shape or label components.
// Highcharts: include percentage in point descriptions
accessibility: { point: { valueDescriptionFormat: '{point.name}: {point.y} tickets, {point.percentage:.0f} percent' } }
Worked example
Permalink to "Worked example"A support dashboard showed “Tickets by priority” as a donut with seven slices in shades of red and a legend. In user testing, colour-blind agents misread High and Urgent, and a screen reader user heard “image”. The redesign kept the donut for four priorities (merging rare ones into “Other”), added direct labels with percentages, white separators and a dotted pattern on one slice, a centre total as text, focusable slices with full names, and a two-column table below. The summary sentence — “Urgent is 9% of open tickets, up from 5% last week” — became the most-read part of the widget.
Writing the summary
Permalink to "Writing the summary"A pie’s summary answers the question the pie was chosen for: what is the biggest part, and how big? Lead with the largest share as a percentage, add the smallest or the notable one, and mention change if the dashboard compares periods: “Open tickets are the largest share at 34%; Waiting is smallest at 16%, down from 22% last week.” Avoid listing every slice in the summary — that is what the table is for — and avoid describing colours or positions.
Testing checklist
Permalink to "Testing checklist"Design system notes
Permalink to "Design system notes"Provide a PartToWholeChart that switches automatically from donut to bars above a slice threshold, renders direct labels, separators and patterns, and includes the table and summary. Teams choose data, not accessibility.
FAQ
Permalink to "FAQ"Are pie charts inherently inaccessible?
No. With few slices, direct labels, a summary and a table they can be fully accessible. They are often not the clearest chart, which is a separate question.
Pie or donut?
Equivalent for accessibility. A donut’s centre can hold a total as text, which is useful.
Should every slice be a tab stop?
One tab stop with arrows between slices is kinder; individual tab stops are acceptable for three or four slices.
How do I make slices distinguishable in forced colours mode?
Use stroke separators that map to system colours and patterns; test with forced-colors emulation.
What should the summary say?
The largest part, the smallest or notable part, and any change that matters: “Open is 34%, the largest share.”
Do I need a legend at all?
Not if slices are labelled directly. If a legend remains, make it text, not colour swatches alone.
Related
Permalink to "Related"- Accessible stacked bar charts — parts of a whole across categories
- Direct labelling instead of legends — labelling technique
- Show-as-table toggles — the table alternative