How to make canvas charts accessible

The real-DOM overlay pattern: per-point screen-reader access on a <canvas> chart, without giving up 60fps.

To a screen reader, a canvas chart is a single opaque rectangle. No values, no keyboard path, nothing find-in-page can reach. The usual advice — "add an aria-label" — turns a thousand data points into one sentence, which fails the moment a user asks the obvious question: what was the value on Tuesday?

The common conclusion is that you must switch to SVG and render a DOM node per point. That works until it doesn't: at tens of thousands of points, per-point DOM is why accessible charts have a reputation for being slow. But the trade-off isn't fundamental. The fix is to keep canvas for pixels and build a small, real DOM for meaning.

What assistive technology actually needs

The overlay pattern, piece by piece

Everything sits inside the chart container, positioned over the canvas:

<div class="chart-root">
  <canvas aria-hidden="true"></canvas>      <!-- pixels only -->
  <div role="application" tabindex="0"        <!-- the keyboard surface -->
       aria-label="Revenue over time. Arrow keys move between samples…"
       aria-describedby="summary" aria-details="data-table"></div>
  <div class="ticks">…real text nodes…</div>
  <div aria-live="polite" aria-atomic="true" class="visually-hidden"></div>
  <p id="summary" class="visually-hidden">Revenue ranges 180–290, now 287, up 12%.</p>
  <table id="data-table" class="visually-hidden">…downsampled rows…</table>
</div>
The trap to avoid: a bare, completely inaccessible canvas scores zero axe-core violations. Automated scanners can't see inside a canvas, so they can't see the problem — an axe-clean chart page proves nothing about the chart. Verification has to be functional: does keyboard navigation work, does the live region announce, does the table exist? See auditing any chart in one command.

Details that separate "has ARIA" from "works in a screen reader"

Steal the implementation

All of the above ships by default in fcharts (MIT, zero dependencies, ~21 KB gzip): the overlay layer is a dozen small files under src/a11y/, renderer-agnostic, and liftable into other canvas charts. The live demo runs 100k points at 60fps with everything described here turned on.