Accessible Chart.js Configuration
Permalink to "Accessible Chart.js Configuration"Chart.js renders into a single <canvas> element. To assistive technology, a canvas is one opaque box: no bars, no points, no axes, no text — unless you add them. Chart.js ships no built-in accessibility layer beyond passing through attributes you set on the canvas, so a default Chart.js chart is announced as nothing, or at best as “graphic”.
This page configures Chart.js (v4) for accessibility in five layers: a name, a summary, a data table, keyboard navigation through the tooltip API, and colour and point-style settings. It belongs to data visualization & chart alternatives.
Spec reference
Permalink to "Spec reference"Canvas content is not exposed to the accessibility tree. The HTML standard allows fallback content inside <canvas>…</canvas> for browsers without canvas support, and some browsers expose focusable fallback elements to assistive technology, but support is inconsistent and fallback content must be kept in sync by hand. The dependable approach is:
role="img"andaria-labelon the canvas (or a<figure>with<figcaption>), giving the chart a name (SC 1.1.1).aria-describedbypointing at a text summary.- A real HTML table of the data, adjacent to the chart (SC 1.3.1).
Chart.js APIs used here: chart.setActiveElements([{ datasetIndex, index }]), chart.tooltip.setActiveElements([...], { x, y }), chart.update(), and the plugins.tooltip, elements.point.pointStyle and dataset borderDash options.
Criteria: SC 1.1.1 Non-text Content, SC 1.3.1 Info and Relationships, SC 1.4.1 Use of Color, SC 1.4.11 Non-text Contrast, SC 2.1.1 Keyboard.
When to rely on Chart.js — and when not to
Permalink to "When to rely on Chart.js — and when not to"Chart.js is a reasonable choice for dashboards with standard line, bar and pie charts, provided you add the layers on this page. Its canvas rendering is fast, and the tooltip API is enough to build keyboard navigation.
If a chart needs rich per-point interaction — selection, drill-down, many hundreds of focusable points — an SVG library where each point can be a real DOM element is easier to make accessible, because focus and names come for free.
The misapplication to name is putting the data into the canvas’s fallback content (<canvas>…<table>…</table></canvas>) and considering the job done. Many screen reader and browser combinations never expose it, and sighted keyboard users cannot see it at all.
Annotated code example
Permalink to "Annotated code example"<figure class="chart-figure">
<!-- SC 1.1.1: the canvas has a name; the summary is its description -->
<canvas id="orders" role="img" aria-label="Orders per month, 2026, by channel"
aria-describedby="orders-summary"></canvas>
<figcaption id="orders-summary"></figcaption> <!-- generated below, visible -->
</figure>
<details class="chart-data">
<summary>Show data table for Orders per month</summary>
<table id="orders-table"><caption>Orders per month, 2026, by channel</caption></table>
</details>
const chart = new Chart(document.getElementById('orders'), {
type: 'line',
data: {
labels: months,
datasets: [
// SC 1.4.1 + 1.4.11: distinct colour, point style AND dash per series
{ label: 'Web', data: web, borderColor: '#155c38', pointStyle: 'circle', borderDash: [] },
{ label: 'Retail', data: retail, borderColor: '#8a6000', pointStyle: 'rectRot', borderDash: [6, 4] },
],
},
options: {
animation: matchMedia('(prefers-reduced-motion: reduce)').matches ? false : undefined,
plugins: { tooltip: { enabled: true }, legend: { labels: { usePointStyle: true } } },
},
});
// SC 1.1.1: a generated summary so it always matches the data
document.getElementById('orders-summary').textContent = summarise(months, { Web: web, Retail: retail });
// e.g. "Web orders rose from 820 in January to 1,410 in December, peaking in November.
// Retail stayed between 300 and 420."
renderTable(document.getElementById('orders-table'), months, { Web: web, Retail: retail }); // SC 1.3.1
// SC 2.1.1: keyboard navigation through Chart.js's own active-element API
const canvas = chart.canvas;
canvas.tabIndex = 0;
let ds = 0, idx = 0;
canvas.addEventListener('keydown', (e) => {
const n = chart.data.labels.length, m = chart.data.datasets.length;
if (e.key === 'ArrowRight') idx = Math.min(idx + 1, n - 1);
else if (e.key === 'ArrowLeft') idx = Math.max(idx - 1, 0);
else if (e.key === 'ArrowDown') ds = (ds + 1) % m;
else if (e.key === 'ArrowUp') ds = (ds - 1 + m) % m;
else if (e.key === 'Escape') { chart.setActiveElements([]); chart.tooltip.setActiveElements([], {}); chart.update(); return; }
else return;
e.preventDefault();
const active = [{ datasetIndex: ds, index: idx }];
chart.setActiveElements(active);
const el = chart.getDatasetMeta(ds).data[idx];
chart.tooltip.setActiveElements(active, { x: el.x, y: el.y }); // visible tooltip follows keys
chart.update();
announce(`${chart.data.datasets[ds].label}, ${chart.data.labels[idx]}: ${chart.data.datasets[ds].data[idx]} orders.`);
});
The summary is generated from the data on every update. A hand-written aria-label like “Chart showing orders” is stale the moment data changes and says nothing about the content. Generation rules — trend, extremes, notable changes — are covered in generating accessible text alternatives for D3 charts and apply unchanged to Chart.js.
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Key / event | Visual | Announcement |
|---|---|---|
Tab to canvas |
Focus ring on canvas | “Orders per month, 2026, by channel, image” then the summary |
Right Arrow |
Tooltip on next month | “Web, February: 910 orders.” |
Down Arrow |
Switch to next series | “Retail, February: 330 orders.” |
Escape |
Tooltip hides | None |
| Open data table | Table visible | Table caption and size |
Integration context
Permalink to "Integration context"The table can be visible, collapsed in a <details> as above, or behind a toggle; the trade-offs are in show-as-table toggles for charts. The keyboard model is the canvas pattern from adding keyboard navigation to canvas bar charts, with Chart.js’s API doing the drawing.
For dark mode, Chart.js does not read CSS custom properties automatically; resolve them with getComputedStyle when creating the chart and on theme change, and call chart.update() — covered for contrast in non-text contrast for chart series.
Gotchas
Permalink to "Gotchas"responsive: true and summaries. Resizing does not change data, but data updates through chart.data do; regenerate the summary and table in the same function that updates the chart.
Legend clicks. Chart.js legends toggle series on click but are drawn on the canvas — not keyboard operable. Hide the canvas legend and render an HTML legend with checkbox toggles.
Pie charts. Slice navigation works the same way (one dataset, many indices); announce the percentage as well as the value.
Design system notes
Permalink to "Design system notes"Wrap Chart.js in a design-system chart component that requires a title and a data model, generates the summary and table itself, installs the keyboard handler, resolves colour tokens per theme, and renders an HTML legend. Products pass data and formatters; the wrapper guarantees the five layers.
Testing checklist
Permalink to "Testing checklist"FAQ
Permalink to "FAQ"Is Chart.js accessible?
Not by default. It draws to a canvas that assistive technology cannot read. You can make a Chart.js chart accessible by naming the canvas, adding a generated text summary, providing a data table, adding keyboard navigation through its active-element and tooltip APIs, and using point styles so series do not rely on colour.
Should I put the data table inside the canvas element as fallback content?
No. Fallback content is exposed inconsistently and is invisible to sighted keyboard users. Render the table next to the chart, visible or behind a disclosure.
Can Chart.js tooltips be shown from the keyboard?
Yes. Call setActiveElements on the chart and on chart.tooltip with the dataset and index, then update the chart, from your own keydown handler.
How do I make Chart.js series distinguishable without colour?
Give each dataset a different pointStyle and borderDash, enable usePointStyle in the legend, and choose colours with at least 3:1 contrast against the background.
Related
Permalink to "Related"- Show-as-table toggles — the table alternative
- Keyboard navigation for canvas bar charts — the keyboard model underneath
- Patterns and shapes for charts — colour-independent series