Accessible Heatmaps

Permalink to "Accessible Heatmaps"

Heatmaps — activity by hour and weekday, error rates by service and region, correlation matrices, calendar contribution grids — encode a number as a colour in each cell. Colour lightness is a weak channel for precise reading even for people with typical vision, and the hue-based palettes many libraries default to (rainbow, red-green) are unreadable for colour-blind users. For screen reader users, a heatmap drawn on canvas is nothing at all. Yet a heatmap is a table at heart: rows, columns and a value in each cell. Built that way, it becomes one of the more accessible chart types.

This guide belongs to chart type-specific patterns.

Spec reference

Permalink to "Spec reference"
  • SC 1.3.1 Info and Relationships (A) — row and column headers for every cell; a table or grid structure.
  • SC 1.4.1 Use of Color (A) — colour cannot be the only way the value is conveyed; show numbers, or bins named in a legend with patterns.
  • SC 1.4.3 Contrast (AA) — any text on coloured cells (values) needs 4.5:1, which usually means switching text colour by cell lightness.
  • SC 1.4.11 Non-text Contrast (AA) — if adjacent cells must be distinguished to understand the chart, consider borders.
  • SC 2.1.1 Keyboard (A) — hover values must be reachable by focus.
A heatmap built as a grid with values Grid mock of error rates by service and region: services as rows, regions as columns, each cell showing a percentage, with high values flagged and one cell focused. A heatmap built as a grid with valuesServiceEUUSAPACCheckout0.2%0.4%3.1%Search0.1%0.3%0.6%Payments0.5%2.4%0.7%Flagged cells exceed the 2% error budget; colour intensity repeats the same information.
Values in cells make colour a secondary cue.

When to use — and when not to

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

Use a heatmap to show patterns across two categorical (or binned) dimensions — when things happen, where errors cluster. Always include values (in cells or on focus) and a summary of the pattern.

Do not use a heatmap when users need exact comparisons between a few values; a bar chart is clearer. Do not use rainbow or red-green palettes. The misapplication: a 24 × 7 activity heatmap with hover-only values and a gradient legend without numbers.

Rainbow heatmap versus accessible heatmap Comparison: a rainbow palette with hover-only values fails colour-blind and keyboard users; an accessible heatmap uses a sequential palette, values in cells, a binned legend and keyboard navigation. Rainbow heatmap versus accessible heatmap✗ Rainbow palette, hover valuesHue changes misread by colour-blind usersValues only on mouse hoverGradient legend without numbersCanvas image✓ Sequential palette, values shownLightness order readable by mostValues in cells or on focusBinned legend: "0–1%", "1–2%", "over 2%"Grid with headers

Annotated code example

Permalink to "Annotated code example"
<table class="heatmap" aria-describedby="hm-sum">
  <caption>Error rate by service and region, last 24 hours</caption>
  <thead><tr><th scope="col">Service</th><th scope="col">EU</th><th scope="col">US</th><th scope="col">APAC</th></tr></thead>
  <tbody>
    <tr>
      <th scope="row">Checkout</th>
      <!-- Value as text; bin class drives colour and text contrast (SC 1.4.1, 1.4.3) -->
      <td class="bin-0">0.2%</td><td class="bin-0">0.4%</td>
      <td class="bin-3">3.1% <span class="flag">over budget</span></td>
    </tr>
  </tbody>
</table>
<p id="hm-sum">2 cells exceed the 2% error budget: Checkout in APAC (3.1%) and Payments in US (2.4%).</p>
/* Sequential palette with increasing darkness; text colour flips for contrast */
.heatmap td.bin-0 { background: #edfdf4; color: #111714; }
.heatmap td.bin-1 { background: #a8f0c8; color: #111714; }
.heatmap td.bin-2 { background: #219150; color: #ffffff; }
.heatmap td.bin-3 { background: #0d3320; color: #ffffff; font-weight: 700; }
.heatmap td { border: 1px solid #ffffff; min-inline-size: 4.5ch; text-align: end; }

For large heatmaps where cells are too small for text, render values on focus and in the table view, and keep the bins few (four to six) so the legend can name them.

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Event Expected announcement AT deviations
Table navigation “APAC, Checkout, 3.1% over budget” Headers read on change
Interactive grid variant, arrows Same, via roving focus —
Summary Hot spots named —
Forced colours Backgrounds lost; values and “over budget” remain That is why values must be text
Palette choices for heatmaps Matrix of palettes — rainbow, red to green, single-hue sequential, diverging with neutral midpoint — against colour-blind safety, ordering, dark mode and suitability. Palette choices for heatmapsColour-blindsafePerceived orderDark modeUse forRainbowNoNoPoorAvoidRed to greenNoPartialPoorAvoidSingle-hue sequentialYesYesInvert lightnessMagnitudesDiverging, neutral middleIf chosen wellYesTuneAbove or below atarget
Single-hue sequential palettes are the safest default.

Integration context

Permalink to "Integration context"

Palette selection is covered in accessible sequential and diverging palettes, and dark-mode variants in dark mode chart palettes. For interactive heatmaps, cell navigation follows implementing roving tabindex for custom data grids. Geographic heatmaps (choropleths) are covered in table alternatives for choropleth maps.

Gotchas

Permalink to "Gotchas"

Too many bins. Continuous colour scales cannot be named. Bin into four to six labelled ranges for the legend, even if the drawing uses a continuous scale.

Correlation matrices. Diagonals and mirrored halves duplicate information; show only one triangle in the table version.

Calendar heatmaps. Contribution-style grids should announce dates in full (“Tuesday 2 September, 7 commits”), not grid coordinates.

Common mistakes

Permalink to "Common mistakes"
  • Rainbow or red-green palettes.
  • Values only on hover.
  • Gradient legends with no numbers.
  • Text on dark cells that fails contrast.

Framework notes

Permalink to "Framework notes"

ECharts heatmaps support visualMap with pieces for binned legends and aria for generated descriptions; enable label.show for values. Highcharts heatmaps with the accessibility module expose cells to keyboard navigation; set dataLabels.enabled and a sequential colorAxis. Nivo and D3 heatmaps render SVG cells; add role, names and focus handling, or render an HTML table as the primary view.

// ECharts: binned legend and values in cells
visualMap: { type: 'piecewise', pieces: [{ lt: 1, label: 'Under 1%' }, { gte: 1, lt: 2, label: '1–2%' }, { gte: 2, label: 'Over 2%' }] },
series: [{ type: 'heatmap', label: { show: true, formatter: p => p.value[2] + '%' } }]

Worked example

Permalink to "Worked example"

An observability tool’s latency heatmap (services × minutes) used a rainbow palette and hover tooltips. Two engineers with deuteranopia could not distinguish “slow” from “normal”. The team switched to a single-hue sequential palette with five named bins, rendered a 15-minute aggregated table view with values for screen reader users, added keyboard focus with “Checkout, 10:42, p95 820 ms, slow” announcements, and summarised hot spots at the top. The rainbow version is gone; nobody asked for it back.

Testing checklist

Permalink to "Testing checklist"

Design system notes

Permalink to "Design system notes"

Ship a heatmap component that renders an HTML table by default, with binned palette tokens verified for contrast in both themes and text colours chosen automatically per bin. Offer a canvas mode only for very large heatmaps, paired with an aggregated table.

FAQ

Permalink to "FAQ"
Can a heatmap be an HTML table?

Yes, and for moderate sizes that is the most accessible option: headers, values and colour together.

How do I handle very large heatmaps?

Aggregate for the accessible table (by hour instead of minute), keep the full-resolution drawing for visual overview, and provide focus-based details.

Which palette should I use?

A single-hue sequential palette for magnitudes; a diverging palette with a neutral midpoint for values above and below a reference.

Do cells need borders?

Thin borders help distinguish adjacent cells, especially in forced colours mode.

Should values always be shown?

Whenever space allows. Otherwise on focus and in the table.

How should the summary be phrased?

Name the extremes and patterns: “Errors peak in APAC between 02:00 and 04:00; all other regions stay below 1%.”

Should a heatmap's table be sortable?

Sorting by a column (a region, an hour) helps users find the worst cells quickly. Keep the original order as the default so the table matches the drawing.

Permalink to "Related"

← Back to Chart Type-Specific Patterns