Adding Keyboard Navigation to Canvas Bar Charts

Permalink to "Adding Keyboard Navigation to Canvas Bar Charts"

A canvas chart is a picture. There is nothing inside it to focus, nothing to name, and no hit testing except what you write. Making one keyboard navigable therefore is not a matter of adding attributes to the canvas — it is a matter of building a parallel structure in the DOM that carries the interaction, and repainting the canvas to follow it. This page covers that structure, the announcement copy for a focused bar, and the focus indicator that has to be drawn rather than styled.

It is the canvas counterpart to adding keyboard navigation to SVG line charts, and it belongs to canvas and SVG chart keyboard interaction.

Spec reference

Permalink to "Spec reference"

The HTML Living Standard describes fallback content inside a canvas element as the element’s fallback content, exposed to assistive technology when the canvas is not rendered. In practice fallback content is unreliable for interaction: browsers vary in whether they expose focusable fallback elements, and the canvas hit-testing API for focus rings is not widely supported.

The dependable pattern is therefore a sibling structure rather than fallback content: a list of real focusable elements, one per data point, positioned over the canvas or visually hidden, with the canvas marked aria-hidden="true".

SC 2.1.1 Keyboard (Level A) requires the interaction to exist at all; SC 1.1.1 Non-text Content (Level A) requires the chart to have a text alternative, which the parallel list plus a summary in the caption provides; SC 2.4.7 Focus Visible (Level AA) requires the focused bar to be visibly indicated, which is the repaint.

Canvas versus SVG for keyboard interaction Comparison of what SVG provides natively for chart keyboard interaction against what canvas requires you to build. Canvas versus SVG for keyboard interaction✓ SVG gives youElements that can take focus directlyCSS focus indicatorsNames from title childrenHit testing for free✗ Canvas requiresA parallel DOM structure to focusA hand-drawn focus indicatorNames on the parallel elementsA redraw on every focus change
Everything canvas lacks has to be rebuilt in the DOM beside it.

When to use — and when not to

Permalink to "When to use — and when not to"

Use canvas when the data volume genuinely demands it — tens of thousands of points, or a chart that animates continuously. Below a few thousand marks, SVG is simpler and accessible almost for free, as the SVG page describes.

Do not build the parallel list for a decorative chart that duplicates a table already on the page. Mark it aria-hidden and let the table be the accessible representation.

Do not attempt hit testing against mouse coordinates as the keyboard path. The focused index is state you already own; deriving it from geometry is more code and more bugs.

Annotated code example

Permalink to "Annotated code example"
<!-- SC 1.1.1: the figure carries the name and the summary -->
<figure>
  <figcaption id="orders-caption">
    Orders by month, 2026. Rose from 210 in January to 412 in June.
  </figcaption>

  <!-- SC 4.1.2: the canvas is a picture; the list is the widget -->
  <canvas id="orders-canvas" width="720" height="320" aria-hidden="true"></canvas>

  <ul class="chart-points visually-hidden" aria-labelledby="orders-caption">
    <li><button type="button" data-index="0">January, 210 orders</button></li>
    <li><button type="button" data-index="1">February, 264 orders</button></li>
    <!-- one per data point, generated from the same array the chart draws -->
  </ul>
</figure>
// SC 2.1.1 + 2.4.7: focus moves natively; the canvas is repainted to follow
const buttons = [...list.querySelectorAll('button')];
let focusedIndex = -1;

list.addEventListener('focusin', (e) => {
  focusedIndex = Number(e.target.dataset.index);
  draw(data, focusedIndex);          // repaint with the highlight
});

list.addEventListener('keydown', (e) => {
  let next = focusedIndex;
  if (e.key === 'ArrowRight') next = Math.min(focusedIndex + 1, data.length - 1);
  else if (e.key === 'ArrowLeft') next = Math.max(focusedIndex - 1, 0);
  else if (e.key === 'Home') next = 0;
  else if (e.key === 'End') next = data.length - 1;
  else return;
  e.preventDefault();
  buttons[next].focus();             // the browser announces the button name
});

function draw(data, highlight) {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  data.forEach((d, i) => {
    ctx.fillStyle = barColour;
    ctx.fillRect(x(i), y(d.value), barWidth, height(d.value));
    if (i === highlight) {
      // SC 2.4.7: the focus indicator must be DRAWN — CSS cannot reach inside a canvas
      ctx.lineWidth = 3;
      ctx.strokeStyle = focusColour;   // 3:1 against both the bar and the background
      ctx.strokeRect(x(i) - 2, y(d.value) - 2, barWidth + 4, height(d.value) + 4);
    }
  });
}

Generating the button labels from the same array the chart draws from is what keeps the two representations in agreement. A hand-written list drifts the first time the data changes shape.

Giving a canvas chart a keyboard surface Three-step approach for canvas charts: render a parallel focusable list of bars, drive focus through it natively, and repaint the highlight from the focused index. Giving a canvas chart a keyboard surfaceRender a parallel listone focusable button per bar, visually hiddenReal elements, real focus, no hit testingFocus moves nativelyTab and arrow keys work on the buttonsThe browser does the workRepaint the canvasredraw the highlight for the focused indexFocus indication must be drawn by hand
The list is the real widget; the canvas is a picture that follows it.

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
The bar chart key contract Keyboard contract for a canvas bar chart: arrows move between bars, Home and End reach the extremes, Enter drills in. The bar chart key contractRight / Left arrowMove to the next or previous bar"March, 412 orders" — label and value togetherHome / EndFirst or last barUseful for reaching extremes without steppingEnterDrill into the focused barAnnounce where the user has landedEscapeLeave the chartFocus returns to the chart container, not the pagetop
Announce the category and the value together — a bare number means nothing by ear.
Assistive technology On focusing a bar Deviation
NVDA + Firefox Reads the button name — category and value None significant
JAWS + Chrome Reads the button name and its position in the list Verbose with long category names
VoiceOver + Safari Reads the button name May not scroll a visually hidden button into view
TalkBack + Chrome Swipes through the buttons in order Works well — the list is the accessible chart

Integration context

Permalink to "Integration context"

The parallel list is also the text alternative required by SC 1.1.1, which means a canvas chart built this way needs no separate hidden table — the list is the data, in order, with labels. Where the chart has several series, group the buttons by series with a labelled ul per series so the reader announces which one it is in.

If the chart updates live, regenerate the buttons from the same update that redraws the canvas, and follow the announcement discipline in real-time data stream announcements rather than announcing every repaint.

Gotchas

Permalink to "Gotchas"

The visually hidden list is not scrolled into view. A clip-based hiding technique keeps the element at a fixed position, so the browser cannot scroll to it. Position each button over its bar instead, with transparent backgrounds, so scrolling and focus behave normally.

The canvas is not repainted on focus. The reader announces a bar the user cannot see highlighted, which is disorienting for a sighted keyboard user.

Focus indicator drawn at low contrast. The outline must clear 3:1 against both the bar and the chart background — measure it, as described in auditing focus indicator contrast for WCAG 2.4.11.

Buttons regenerated on every frame. Rebuilding the list during an animation destroys focus. Update text content in place instead.

FAQ

Permalink to "FAQ"
Can a canvas element be made keyboard navigable directly?

Not usefully. A canvas is a single element that draws pixels; there is nothing inside it to focus, so keyboard navigation between data points has to be provided by real DOM elements. The established approach is a parallel list of focusable elements — one per data point — kept visually hidden, with the canvas repainted to show which one is focused.

Does the canvas need aria-hidden?

Yes, if a parallel accessible structure exists. Otherwise assistive technology may announce the canvas as an unlabelled graphic in addition to the list, which duplicates the content. Mark the canvas aria-hidden=“true” and let the parallel list and the figcaption carry the meaning.

How should the focused bar be indicated visually?

Repaint it. Because the focusable element is not the canvas, the browser draws no focus ring on the bar itself, so the highlight has to be part of the render. Draw a clear outline around the focused bar at 3:1 contrast against both the bar colour and the chart background, and keep the visually hidden button positioned over the bar so browser scrolling behaves.

Permalink to "Related"

Back to Canvas & SVG Chart Keyboard Interaction