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" and aria-label on the canvas (or a <figure> with <figcaption>), giving the chart a name (SC 1.1.1).
  • aria-describedby pointing 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.

Five layers around a Chart.js canvas Layers added around a Chart.js canvas: name, generated summary, data table, keyboard navigation through the tooltip API, and colour plus point styles. Five layers around a Chart.js canvasNamerole="img" and aria-label on the canvas, or figure + figcaptionSummarygenerated sentence referenced by aria-describedbyData tablereal table beside the chart, or behind a toggleKeyboardarrow keys drive setActiveElements and the tooltipEncoding3:1 colours, pointStyle and borderDash per series
Chart.js supplies the pixels; everything in these layers is configuration and a few lines of glue.

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
Default Chart.js versus configured Chart.js Comparison of what assistive technology users get from a default Chart.js chart against a chart configured with a name, summary, table, keyboard navigation and colour-independent series. Default Chart.js versus configured Chart.js✗ DefaultCanvas announced as nothing or "graphic"No summary, no dataTooltip on hover onlySeries distinguished by colour alone✓ ConfiguredNamed image with a generated summaryData table beside the chartArrow keys drive the tooltipPoint styles and dashes per series
None of the right-hand column requires a plugin — it is attributes, options and a keydown handler.

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.

Chart.js options with accessibility impact Matrix of Chart.js options and attributes relevant to accessibility, what each does, and the criterion it supports. Chart.js options with accessibility impactSettingEffectCriterionrole="img" + aria-labelCanvas gets a name1.1.1aria-describedby →summaryTrend described in text1.1.1pointStyle, borderDashSeries not colour-only1.4.1legend usePointStyleLegend matches marks1.4.1animation false on reduceNo motion for opted-out users2.3.3
Most of the work is in attributes around the canvas; a few options remove colour-only encoding.

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.

Permalink to "Related"

← Back to Data Visualization & Chart Alternatives