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.
Tickets by status as a bar alternative Bar chart showing the same data as a pie: Open 412, In progress 305, Waiting 188, Resolved 299. Tickets by status as a bar alternativeOpen34%In progress25%Resolved25%Waiting16%
The same parts-of-a-whole data as bars; often easier to compare than pie angles.

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.

Legend-keyed donut versus labelled donut Comparison: a legend-keyed donut with many slices relies on colour matching; a labelled donut with few slices, direct labels, separators and a centre total works for more users. Legend-keyed donut versus labelled donut✗ Legend-keyed, 12 slicesMatch colour to legendSimilar huesCentre value is an imageNo table✓ Labelled, 5 slices + OtherLabel on or beside each sliceSeparators and patternsCentre total is real textTable and summary sentence

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
Pie readability by slice count Matrix of slice counts — two to three, four to six, seven to ten, more than ten — against angle comparison, label fit, colour distinction and recommendation. Pie readability by slice countAngle comparisonLabels fitColour distinctionRecommendation2 to 3EasyYesEasyPie fine4 to 6ModerateMostlyNeeds patternsPie with labels7 to 10HardCrowdedHardUse barsMore than 10Very hardNoNoUse bars or table
Beyond six slices, switch to bars.

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.

Permalink to "Related"

← Back to Chart Type-Specific Patterns