Skip to content
Back to blog
WCAGhow-totables

Accessible data tables: a practical guide

How to build accessible data tables: th and scope, captions, complex headers, sortable columns and responsive layouts, with the WCAG criteria.

P

Pavel Charkasau

Accessible data tables come down to one idea: a screen reader has to know which header belongs to each cell. A sighted user gets that from the grid on screen. A screen reader user gets it only from the markup. So use a real <table>, mark header cells with <th> and data cells with <td>, give each header a scope of col or row, and name the table with a <caption>. For tables with headers at more than one level, the W3C tutorial says to switch to id and headers attributes and link each cell to its headers explicitly (W3C WAI Tables Tutorial). The criterion you are measured against is 1.3.1 Info and Relationships at Level A, which requires that structure conveyed through presentation "can be programmatically determined or are available in text" (W3C). This guide covers the markup first, then the three places tables usually break in real products: complex headers, sortable columns and small screens.

What makes a data table accessible?

A data table is accessible when a screen reader user can move into any cell and hear which row and column it belongs to. When they arrow into the cell containing "€49", a good table announces "Pro, Monthly price, €49". A broken one just says "€49", and on row 14 of 30 that number means nothing.

Screen readers do this by reading the relationships in the markup. The W3C tutorial states the reason directly: "With structural markup, headers and data cells can be programmatically determined by software" (W3C WAI). A grid of <div>s styled to look like a table has no such relationships. Neither does a <table> whose top row is made of <td> cells with bold text. Both look right and both fail 1.3.1. The W3C lists the second one as a named failure: F91, not correctly marking up table headers (W3C).

The EU standard says the same thing. EN 301 549 clause 9.1.3.1 carries WCAG's Info and Relationships over word for word for web content, so a table that fails 1.3.1 also fails the clause the EAA and the Web Accessibility Directive rely on (ETSI EN 301 549 v3.2.1). In the US, the Revised Section 508 Standards incorporate WCAG 2.0 Level A and AA by reference, so 1.3.1 applies to federal sites too (U.S. Access Board).

How do I mark up a simple data table?

Here is a plan comparison table. It has one row of column headers and one column of row headers, which describes most tables on a SaaS or ecommerce site:

<table>
  <caption>Plan prices, billed in euros</caption>
  <thead>
    <tr>
      <th scope="col">Plan</th>
      <th scope="col">Monthly price</th>
      <th scope="col">Pages per scan</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Starter</th>
      <td>€19</td>
      <td>100</td>
    </tr>
    <tr>
      <th scope="row">Pro</th>
      <td>€49</td>
      <td>1,000</td>
    </tr>
  </tbody>
</table>

(The numbers are made up for the example.) Two decisions matter here.

The first cell of each row is a <th scope="row">, not a <td>. That is what lets a screen reader say "Pro" before the price. If your first column holds the thing each row is about, it is a row header.

scope is technically optional on a simple table, since browsers infer it. MDN's reference notes that "some assistive technologies may fail to draw correct inferences, so specifying header scope may improve user experiences" (MDN). It costs you one attribute per header, so I always add it.

Do I need a caption, and what goes in it?

Use a <caption>. It is the table's accessible name, and screen readers read it when a user lands on the table or jumps between tables. MDN puts the reason well: a caption that describes the table's purpose "helps the people decide if they need to check the rest of the table content or skip over it" (MDN). The W3C technique for this is H39 (W3C).

Keep it short and specific. "Plan prices, billed in euros" works. "Table 1" doesn't. If the visible heading above the table already says the same thing, you can point the table at it with aria-labelledby instead of repeating the text, though a real <caption> is the simpler default.

Don't use the summary attribute. It is deprecated in HTML, and MDN says to use <caption> instead (MDN). If a complex table needs reading instructions, put them in the caption or in a paragraph linked with aria-describedby.

How do I handle tables with complex or multi-level headers?

Try to split the table first. Two simple tables are usually easier for everyone to read than one with merged, two-level headers. MDN gives the same advice and only reaches for headers "if the table cannot be broken apart" (MDN).

If you can't split it, the W3C tutorial gives two steps up from basic scope:

  • Irregular headers, where a header spans a group of columns or rows: use scope="colgroup" or scope="rowgroup" on the spanning header, with matching <colgroup> or <tbody> elements to define the group.
  • Multi-level headers, where a cell needs more than one header from the same direction: give each <th> an id and list the relevant ids on each <td> in its headers attribute (W3C WAI Tables Tutorial).
<th id="q3" colspan="2" scope="colgroup">Q3 2026</th>
...
<th id="q3-issues" scope="col">Issues found</th>
...
<td headers="site-shop q3 q3-issues">214</td>

The headers approach works, but it breaks easily. The W3C has a named failure for it, F90, incorrectly associating table headers via headers and id. The usual cause is a template that renames or drops an id while the cells still point at the old one. The axe-core rule td-headers-attr reports cells whose headers value refers to something outside the same table (Deque University). A scanner can tell you an id is missing, though. It can't tell you the id points at the wrong header.

Should layout tables ever use th?

No. If you still have a table used only for layout, typically in HTML email, it must not contain <th>, <caption> or scope. The W3C lists <th> in a layout table as failure F46 (W3C). Add role="presentation" to the layout <table> so screen readers skip the table semantics. On the web itself, the W3C tutorial's advice is to use CSS for layout, not tables (W3C WAI).

How do I make a sortable table accessible?

Follow the W3C's sortable table example. Put the header text inside a <button> within the <th>, and put aria-sort only on the column that is currently sorted: "When the sorted column is changed, the aria-sort attribute is removed and set on the newly sorted column" (W3C ARIA APG).

<th scope="col" aria-sort="descending">
  <button type="button">
    Issues found <span aria-hidden="true">▼</span>
  </button>
</th>

The arrow is aria-hidden so it doesn't end up in the button's name. The APG example also adds visually hidden text to the caption explaining that column headers with buttons are sortable.

A common mistake is putting aria-sort="none" on every column header. The APG sets it on one header at a time, and repeating "not sorted" on eight columns is noise.

Should an interactive table be a grid instead?

Usually not. A table is for reading data. It has no keyboard interaction of its own, and users move through it with their screen reader's table commands. The APG's table pattern says authors "are strongly encouraged to use a native HTML table element whenever possible" (W3C ARIA APG).

role="grid" is a composite widget with arrow-key navigation you build and maintain yourself. The APG suggests it for one case: a table where every row has several interactive controls, so the Tab sequence gets very long. A grid lets the whole table be a single Tab stop. If your table has one "Edit" link per row, keep the table. If it is really a spreadsheet, a grid may be the right pattern, and you're signing up for the keyboard model that comes with it.

How do I make a data table responsive without breaking it?

WCAG gives tables some room here. 1.4.10 Reflow (Level AA) requires content to work at 320 CSS pixels wide without scrolling in two directions, but it excepts "parts of the content which require two-dimensional layout", and data tables are one of the examples it names. The exception covers the table, "not individual cells": text inside a cell still has to wrap (W3C).

So the least risky responsive table is a normal table inside a scrolling wrapper:

<div class="table-scroll" role="region" aria-labelledby="plans-cap" tabindex="0">
  <table>
    <caption id="plans-cap">Plan prices, billed in euros</caption>
    ...
  </table>
</div>

The tabindex="0" matters. A keyboard user can't scroll a container that can't take focus, unless it holds something focusable. The axe-core rule scrollable-region-focusable checks this and maps it to 2.1.1 Keyboard (Deque University).

The other approach is restyling the table with display: block or grid and stacking each row into a card. Be careful with it. For years this removed table semantics in some browsers. Adrian Roselli's long-running test notes that Chromium, Gecko and WebKit all handled it properly by October 2023, with display: contents still a risk (Adrian Roselli). If you stack rows into cards, add the explicit roles (table, rowgroup, row, columnheader, rowheader, cell) back onto the elements and test with a screen reader. My view: scroll the table, don't turn it into cards. Anyone comparing data needs the columns in view, and a stacked card layout removes the thing that made it a table.

What can a scanner catch in a table, and what can't it?

Automated checks catch the structural mistakes: a header cell with no data cells under it (th-has-data-cells, Deque University), broken headers references, a scrolling wrapper nobody can focus. Our scanner runs these rules against the rendered page and gives you the selector for each one.

A tool can't tell whether a <td> in the first column should have been a row header, or whether your caption describes the table. It can't tell whether a headers list points at the right headers, only that they exist. Deque's study of more than 2,000 audits put automated coverage near 57% of issues by volume (Deque), and lower when you count success criteria instead. The W3C says evaluation tools "can not determine accessibility, they can only assist in doing so" (W3C WAI). Full conformance needs a person to check.

The manual check takes a few minutes per table. Turn on a screen reader, move into the middle of the table with its table navigation keys (Ctrl+Alt+arrows in NVDA and VoiceOver on macOS uses VO+arrows), and listen: does each cell announce its row and column header? Then tab through at 320 pixels wide and confirm you can scroll the table from the keyboard. The WCAG checklist lists the related criteria.

FAQ

Do I need scope on every th element?

Not strictly, but add it. Browsers infer scope on simple tables, but MDN notes some assistive technologies infer it incorrectly, so an explicit scope="col" or scope="row" is the safer choice.

Can I build an accessible table with divs and ARIA?

Yes, with role="table", row, columnheader, rowheader and cell, but it's more work for the same result. The W3C's table pattern strongly encourages a native <table> element whenever possible.

Is the table summary attribute still used?

No. The summary attribute is deprecated in HTML. Use a <caption> for the table's name, and a caption or a linked paragraph for any extra reading instructions.

Do data tables have to reflow at 320 pixels?

No. WCAG 1.4.10 Reflow excepts data tables because they need two-dimensional layout, so horizontal scrolling of the table is allowed. The text inside each cell must still wrap, and the scrolling container must be reachable by keyboard.

When should I use role="grid" instead of a table?

Use a grid only when the table works like an application widget, typically when each row has several interactive controls. Grids need arrow-key navigation that you build yourself, so a plain table is the better default for data people read.

Check the tables on your site

Tables are usually generated from one component or template, so one missing scope or broken headers reference tends to show up on every page that uses it. Run a free scan to find the structural issues with their selectors, then take a screen reader through one table by hand and listen for the headers. The scan covers the markup. You still have to listen to what the screen reader announces.


Pavel Charkasau, founder, wcagc.com. Last updated 22 September 2026.

Sources