Accessible Tooltips for Chart Data Points
Permalink to "Accessible Tooltips for Chart Data Points"A chart tooltip is the small box that shows a data point’s exact values — “March: 1,240 orders, up 8%”. In most chart libraries it appears on mouseover and vanishes on mouseout. For keyboard users who arrow through the data, it never appears; for low-vision users who hover and then move the pointer to read a tooltip that is partly off-screen, it vanishes as they approach; for screen reader users it is either silent or read twice.
This page makes the tooltip a presentation of the chart’s focus state, following SC 1.4.13 Content on Hover or Focus. It belongs to canvas & SVG chart keyboard interaction.
Spec reference
Permalink to "Spec reference"SC 1.4.13 Content on Hover or Focus (Level AA) applies to additional content that appears on pointer hover or keyboard focus and hides again. It must be:
- Dismissible — closable without moving hover or focus, usually with
Escape, unless it obscures nothing. - Hoverable — the pointer can move over the tooltip without it disappearing.
- Persistent — it stays until hover or focus is removed, the user dismisses it, or its information is no longer valid.
The ARIA tooltip role describes a popup that displays a description for an element, associated through aria-describedby. For chart points, the tooltip usually carries the point’s primary information rather than a supplementary description, so the more reliable approach is to put the same text in the point’s accessible name (SVG) or a polite live region (canvas), and keep the visual tooltip aria-hidden to avoid double reading.
Related: SC 2.1.1 Keyboard (values available without a pointer), SC 1.4.3 and 1.4.11 (tooltip text and border contrast), SC 2.4.11 Focus Not Obscured (the tooltip must not hide the focused point).
When to use a tooltip — and when a persistent readout
Permalink to "When to use a tooltip — and when a persistent readout"Use a tooltip for occasional detail on a chart whose shape carries the main message. It keeps the chart clean.
Use a persistent readout panel — a fixed area beside the chart that always shows the focused point’s values — for charts users explore intensively, such as financial time series. It never covers the data, never needs dismissing, and is easier to read at high zoom. Many analytics tools offer both.
The misapplication to name is making the tooltip itself focusable and moving focus into it. Focus leaves the chart, arrow-key navigation stops, and the user has to find their way back.
Annotated code example
Permalink to "Annotated code example"// A single focus state drives ring, tooltip and announcement
const state = { index: null, dismissed: false };
function setFocusPoint(i, source) { // source: 'keyboard' | 'pointer'
state.index = i; state.dismissed = false;
render();
if (source === 'keyboard') announce(pointText(i)); // pointer users see the tooltip
}
function render() {
drawChart();
if (state.index === null) return hideTooltip();
const p = points[state.index];
drawFocusRing(p); // SC 1.4.11
if (!state.dismissed) showTooltip(p);
}
function showTooltip(p) {
tip.textContent = pointText(p.i);
tip.hidden = false;
// SC 2.4.11: place clear of the point; flip if near an edge
const { x, y } = toScreen(p);
const above = y - tip.offsetHeight - 16 > 0;
tip.style.left = `${Math.min(Math.max(x - tip.offsetWidth / 2, 8), chartW - tip.offsetWidth - 8)}px`;
tip.style.top = `${above ? y - tip.offsetHeight - 16 : y + 16}px`;
}
// SC 1.4.13 hoverable: moving onto the tooltip does not hide it
let hideTimer;
chart.addEventListener('pointerleave', () => { hideTimer = setTimeout(clearIfNotHovered, 300); });
tip.addEventListener('pointerenter', () => clearTimeout(hideTimer));
tip.addEventListener('pointerleave', () => { hideTimer = setTimeout(clearIfNotHovered, 300); });
// SC 1.4.13 dismissible: Escape hides it without moving focus
chart.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !tip.hidden) { state.dismissed = true; tip.hidden = true; e.stopPropagation(); }
});
<div class="chart" tabindex="0" role="group" aria-label="Orders per month, 2026">
<canvas aria-hidden="true"></canvas>
<!-- visual only: the same text reaches AT through the live region -->
<div class="chart-tip" aria-hidden="true" hidden></div>
</div>
<p class="visually-hidden" role="status" id="chart-live"></p>
Announcing only for keyboard-driven changes is deliberate. Screen reader users navigate with the keyboard; announcing every pointer hover would speak whenever a sighted colleague moved the mouse across a shared screen, and would flood speech for screen magnifier users who also run a reader.
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Event | Visual | Announcement |
|---|---|---|
| Arrow to a point | Ring + tooltip appear | “March: 1,240 orders, up 8%.” |
Escape |
Tooltip hides; ring stays | None |
| Next arrow | Tooltip reappears for the new point | New point announced |
| Pointer hover | Ring + tooltip | None (visual only) |
| Pointer moves onto tooltip | Tooltip stays | None |
| Focus leaves chart | Ring and tooltip hide | None |
Integration context
Permalink to "Integration context"The focus state here is the same one maintained by the chart’s keyboard model — see adding keyboard navigation to canvas bar charts and keyboard navigation for scatter plots. The focus ring’s colour follows non-text contrast for chart series.
With Chart.js, Highcharts and similar libraries, disable the built-in tooltip’s default trigger and call the library’s API to show it at the focused index from your keyboard handler; accessible Chart.js configuration shows the Chart.js calls.
Gotchas
Permalink to "Gotchas"Tooltips at 400% zoom. A tooltip sized in pixels can be larger than the zoomed chart. Size it in rem, cap its width, and let text wrap.
Animated entrances. Fade and slide animations on every arrow press are distracting and slow. Disable them under prefers-reduced-motion.
Stale tooltips. If the data refreshes, the tooltip’s text may no longer be valid; SC 1.4.13 lets it disappear then. Re-render or hide it.
Design system notes
Permalink to "Design system notes"Charts in a design system should share one ChartTooltip component driven by a focus state object, with tokens for background, border and text that meet contrast in both themes, a max width in rem, and built-in Escape and hover handling. Products then supply only pointText(i), which is also the announcement.
Testing checklist
Permalink to "Testing checklist"FAQ
Permalink to "FAQ"Do chart tooltips need to work with the keyboard?
Yes. If exact values are shown only in a hover tooltip, keyboard users cannot get them, which fails SC 2.1.1. Show the tooltip when a point receives keyboard focus, from the same state that drives hover.
What does SC 1.4.13 require of chart tooltips?
That they can be dismissed without moving focus or pointer, usually with Escape; that the pointer can move over the tooltip without it disappearing; and that it stays visible until focus or hover moves away or the user dismisses it.
Should the tooltip be announced by screen readers?
Its information should be, once. Put the text in the focused point’s accessible name or a polite live message, and mark the visual tooltip aria-hidden so it is not read a second time.
Should focus move into the tooltip?
No. Keep focus on the chart so arrow-key navigation continues. The tooltip is a presentation of the focused point, not a separate stop.
Related
Permalink to "Related"- Keyboard navigation for scatter plots — the focus model tooltips follow
- Keyboard navigation for canvas bar charts — canvas focus without DOM elements
- Non-text contrast for chart series — contrast of the focus ring