Table Alternatives for Choropleth Maps

Permalink to "Table Alternatives for Choropleth Maps"

A choropleth map shades each region — country, state, sales territory, postcode — by a value: darker for more revenue, redder for higher incidence. It is one of the most compact ways to show geographic variation, and one of the least accessible. The information is carried entirely by colour intensity, positioned by geography that screen reader users cannot see and low-vision users may struggle to distinguish, with a legend that maps colour bins to ranges.

The accessible equivalent is not a description of the map; it is the data behind it: a table of regions and values, plus a short summary of the pattern the map reveals. This page builds both. It belongs to maps & geospatial data.

Spec reference

Permalink to "Spec reference"
  • SC 1.1.1 Non-text Content (A) — the map needs a text alternative that serves the same purpose. For a data map, the purpose is to convey values by region, so a table is the equivalent; a sentence like “Map of sales by state” is only a name.
  • SC 1.3.1 Info and Relationships (A) — region–value pairs are relationships; a table expresses them.
  • SC 1.4.1 Use of Color (A) — bins distinguished only by shade need another channel on the map (labels, patterns) or the table suffices as the alternative.
  • SC 1.4.11 Non-text Contrast (AA) — region borders and adjacent bins need to be distinguishable.

Region shapes in SVG maps can be individually focusable with names (see the keyboard guide); the table is still needed for comparison and sorting.

The map's data as a table Mock data table of regions with revenue values, the colour bin written as text, and rank, sorted by value. The map's data as a tableRegionRevenueBin1RankNorth2€410kHigh (400k+)1West€355kMid (250–400k)2East€260kMid (250–400k)3South€120kLow (under 250k)41The bin is text — the same ranges as themap legend, readable without colour2Region names are row headers; selecting arow highlights the region on the map
Sorted by value, the table answers the map's main question — where is it highest? — in one read.

When a table is enough — and when to add more

Permalink to "When a table is enough — and when to add more"

A table plus a summary is enough when users need values and rankings: “which territories are above target”, “where is churn highest”. It is also enough for print and export.

Add a pattern summary when geography matters to the question: “the three highest regions are all coastal”, “values fall from north to south”. Tables sorted by value do not show spatial clustering; a sentence can. Generate it from the data and region metadata (neighbours, groupings) rather than writing it by hand.

The misapplication to name is an alt text that describes the colours: “A map with dark green in the north and light green in the south”. It describes the picture instead of the data, and it goes stale the moment the data changes.

Annotated code example

Permalink to "Annotated code example"
<figure class="map-figure" aria-labelledby="rev-map-cap">
  <region-map id="rev-map"></region-map>             <!-- the visual map -->
  <figcaption id="rev-map-cap">
    <strong>Revenue by region, Q1 2026.</strong>
    <span id="rev-map-summary"><!-- generated summary --></span>
  </figcaption>
</figure>

<table id="rev-table">
  <caption>Revenue by region, Q1 2026</caption>             <!-- SC 1.3.1 -->
  <thead><tr>
    <th scope="col">Region</th>
    <th scope="col" aria-sort="descending"><button type="button">Revenue</button></th>
    <th scope="col">Range</th>                                <!-- SC 1.4.1: bin as text -->
    <th scope="col">Rank</th>
  </tr></thead>
  <tbody><!-- generated --></tbody>
</table>
const BINS = [
  { min: 400000, label: 'High (400k and above)', token: '--map-bin-3' },
  { min: 250000, label: 'Mid (250k to 400k)',    token: '--map-bin-2' },
  { min: 0,      label: 'Low (under 250k)',      token: '--map-bin-1' },
];
const binOf = (v) => BINS.find((b) => v >= b.min);

function renderTable(rows) {
  const sorted = [...rows].sort((a, b) => b.value - a.value);
  tbody.replaceChildren(...sorted.map((r, i) => tr(
    th(r.name, { scope: 'row', id: `row-${r.id}` }),
    td(fmtEUR(r.value)), td(binOf(r.value).label), td(String(i + 1)))));
}

// SC 1.1.1: summary from the data, including a geographic pattern
function summarise(rows, regionMeta) {
  const s = [...rows].sort((a, b) => b.value - a.value);
  const top = s[0], low = s.at(-1);
  const highs = s.filter((r) => binOf(r.value) === BINS[0]).map((r) => r.name);
  const coastal = highs.every((n) => regionMeta[n].coastal);
  return `${top.name} is highest at ${fmtEUR(top.value)}; ${low.name} lowest at ${fmtEUR(low.value)}. `
    + `${highs.length} region${highs.length === 1 ? '' : 's'} in the high band`
    + (coastal && highs.length > 1 ? ', all coastal.' : '.');
}

Sorting the table by value by default puts the map’s most important information — the extremes — first. Region-name sorting is one click away for users looking up a specific region.

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Navigation Expected announcement
Figure / caption “Revenue by region, Q1 2026. North is highest at €410k; South lowest at €120k. 1 region in the high band.”
T to the table “Revenue by region, Q1 2026, table with 4 columns and 5 rows”
Down the Revenue column “Revenue, sorted descending, €410k” …
Across a row “North, €410k, High (400k and above), 1”
Activate Revenue sort “Sorted by Revenue, ascending.”
Describing the picture versus providing the data Comparison of a map text alternative that describes colours and shapes against one that provides a data table and a generated pattern summary. Describing the picture versus providing the data✗ Describe the picture"Dark green in the north, pale in the south"No values, no rankingStale when data changesUseless for comparison✓ Provide the dataGenerated from the same dataSortable, copyable, printable
Users of the alternative want what the map tells sighted users — values and patterns — not a description of shading.

Integration context

Permalink to "Integration context"

The table can sit below the map, or behind a toggle as described in show-as-table toggles for charts. The map itself should also be operable — regions reachable by keyboard, pan and zoom without a mouse — covered in keyboard panning and zooming for maps.

Colour bins need 3:1 contrast against their neighbours and the background, or borders between regions; the principles are in non-text contrast for chart series, and the forced-colours treatment of colour scales is in forced colours mode for SVG charts.

One data set, three views Flow from a region data set to the map shading, the table rows and the generated summary, with bins shared between map legend and table. One data set, three viewsRegion dataname, value,metadataBinsranges definedonceMapshading + labelsTablevalue, range, rankSummaryextremes andpattern
Bins are defined once and used by the map, the legend and the table's Range column.

Gotchas

Permalink to "Gotchas"

Tiny regions. Small regions (city states, dense postcodes) are nearly invisible on the map but equal rows in the table — the table is often the only way anyone can read them.

Unequal bins. Legends with bins like “0–10, 10–1,000, 1,000+” mislead visually; the table’s raw values correct that.

Hundreds of regions. A county-level map produces a long table. Add a filter by parent region and keep the sort by value, so the extremes stay at the top.

Design system notes

Permalink to "Design system notes"

A map component should accept the same data model as the table component and render both from it, sharing a bins definition for legend, shading and the Range column. The summary generator can take region metadata (coastal, parent region) as an optional input for pattern sentences.

Testing checklist

Permalink to "Testing checklist"

FAQ

Permalink to "FAQ"
What is an accessible alternative to a choropleth map?

A data table with one row per region, its value and its colour range written as text, sorted by value, plus a short generated summary of the pattern — the highest and lowest regions and any geographic clustering.

Is an alt text description of the map enough?

No. Describing colours and shapes does not give users the values or let them compare regions. The map’s data, as a table, is the equivalent.

How should the colour legend be made accessible?

Write each colour bin as a text range — “High, 400k and above” — in the legend and in a column of the table, so the classification is available without seeing colour.

What if the map has hundreds of regions?

Keep the table, sort it by value so the extremes come first, and add filtering by parent region so users can narrow it down.

Permalink to "Related"

← Back to Maps & Geospatial Data