Command Palette

Search for a command to run...

Hectal
PHASE 3Beginner ~28 min· topic 4 of 4

Topic 3.4

Accessible Tables and When Not to Use Them

In one line

Screen readers link each data cell to its headers; scope makes that link certain for simple tables and headers/id for complex ones. Use tables only for data, never for layout, and plan how a wide table behaves on a phone.

Think of it like this

Finding a seat in a cinema with your eyes closed. A friend whispers "Row F, seat 12" each time you move. Without both labels you'd be lost. A screen reader (software that reads the page aloud for people who are blind or have low vision) does the same in a table: when the user moves to a cell, it reads the row header and the column header, then the value: "Asha, Maths, 92".

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Screen reader
Software that reads a page aloud (or to a braille display) for people who can't see the screen.
Assistive technology
Tools that help disabled people use computers: screen readers, magnifiers, voice control.
scope
An attribute on th that says whether it labels a column, a row, or a group of them.
headers attribute
An attribute on a cell listing the ids of the header cells that label it.
id
A unique name for one element on the page, used to point at it.
Layout table
A table used only to position things on the page, not to show data. Avoid it.
role
An ARIA attribute that tells assistive technology what an element is, like region or presentation.
tabindex="0"
Makes an element reachable with the Tab key, so keyboard users can focus it.

Step by step

01Start with a simple two-way table

Column headers along the top, a row header at the start of each row. This is the most common table, and scope is all it needs.

marks.htmlwhole filehtml
<table>
  <caption>Term 1 marks</caption>
  <thead>
    <tr>
      <td></td>
      <th scope="col">Maths</th>
      <th scope="col">Science</th>
    </tr>
  </thead>
  <tbody>
    <tr><th scope="row">Asha</th><td>92</td><td>88</td></tr>
    <tr><th scope="row">Ravi</th><td>75</td><td>81</td></tr>
  </tbody>
</table>

02Hear what a screen reader hears

Moving to the 88 cell, a screen reader says something like "Asha, Science, 88". Moving down to 81, it says only what changed: "Ravi, 81". The empty top-left cell is a td because it labels nothing.

Hear what a screen reader hearsdiagram
Rendering diagram…

03Use headers and id for a tangled table

Here each region has two sub-rows, and a cell belongs to a quarter, a region and a product. List all three ids in headers.

sales.htmlwhole filehtml
<tr>
  <th id="q1">Q1</th>
</tr>
<tr>
  <th id="north" rowspan="2">North</th>
  <th id="tea">Tea</th>
  <td headers="q1 north tea">120</td>
</tr>

04Turn off table meaning on a layout table

If old code uses a table for layout and you can't rewrite it yet, role="presentation" stops it being announced as a table. New code should use CSS layout instead.

old-page.htmlwhole filehtml
<table role="presentation">
  <tr>
    <td>Menu</td>
    <td>Article text</td>
  </tr>
</table>

05Make a wide table scroll inside a box

Wrap the table. The box scrolls sideways; the page doesn't. The region role and name tell screen reader users what the scrollable area is, and tabindex="0" lets keyboard users focus it and scroll with the arrow keys.

wide.htmlwhole filehtml
<div class="table-scroll" role="region" aria-labelledby="cap" tabindex="0">
  <table>
    <caption id="cap">Phone plans compared</caption>
    ...
  </table>
</div>

Try it yourself

  1. 1

    Change a scope

    In the first example, change Asha's header to scope="col". Predict what the console prints for Asha. The page looks the same, but a screen reader would now treat Asha as labelling the cells below it, which is wrong.

  2. 2

    Follow another cell's headers

    In the second example, move id="target" to the 135 cell. Predict the sentence before you run: which region, product and month should it say?

  3. 3

    Scroll with the keyboard

    In the scrolling example, press Tab until the box gets a purple outline, then use the arrow keys. Now delete tabindex="0" and try again: the box can't be reached by keyboard.

  4. 4

    See the cards

    Make the preview narrower than 480px (or change max-width: 480px to max-width: 2000px). Each loan turns into a card with labels like Due: 16 Oct.

Code & diagrams

Headers with scope New tab

`abbr` gives a long header a short form that some screen readers use when repeating it for every cell.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Maths: scope=col
Science: scope=col
English language: scope=col
Asha: scope=row
Ravi: scope=row
Short name for screen readers: Eng
A complex table with headers and id New tab

The script follows each id in `headers`, like a screen reader does. Without `headers`, nothing would tell it that this Coffee belongs to South, not North.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

The highlighted cell is read as: South, Coffee, Jan, 60
A wide table that scrolls inside its own box New tab

Make the preview narrow (or click the box and press the right arrow key) to scroll the table. `white-space: nowrap` keeps cells on one line so the scrolling is easy to see.

Preview

Press Run to see the page.

Console

Nothing logged yet.
Stacked cards on small screens New tab

Below 480px wide, each row becomes a card and each value shows its column name. Test this pattern with a screen reader: changing `display` can remove table meaning in some browsers.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Point headers at an id that doesn't exist

In the second example, change headers="south s-cof jan" on the target cell to headers="south s-coffee jan".

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Cannot read properties of null (reading 'textContent')

Break #2

Build the table from divs

Replace the marks table with divs laid out by CSS Grid, styled to look identical.

terminal
$ screen reader on the 88 cell
── what you'll see ──
"88" (no row or column name, and no table navigation keys)

Myth vs fact

Myth

Every th needs scope.

Fact

In a simple table with headers only in the first row and first column, browsers work out the links themselves. scope makes it certain and costs nothing, so it's a good habit, but headers/id is only needed for complex tables.

Myth

Tables are bad for accessibility.

Fact

Layout tables are bad. Data tables with proper headers are among the most accessible structures on the web: screen readers offer special keys to move by row and column.

Myth

overflow-x: auto on the table itself makes it scroll.

Fact

A <table> grows to fit its content, so it doesn't scroll by itself. Put the overflow on a wrapper div.

Myth

The old summary attribute explains a table to screen readers.

Fact

summary is obsolete in HTML. Put a short explanation in the caption, or in a paragraph linked with aria-describedby.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Browsers compute a table's accessibility tree from the HTML table model. Chromium and Firefox use heuristics to decide whether a table is for data or layout (for example: has th, caption, thead, or borders). Giving data tables real headers and a caption means you never depend on those guesses.

  • ▸

    Setting display: block or display: grid on table elements has, in some browsers and versions (notably older Safari), removed their table role in the accessibility tree. If you use stacked-card CSS, add explicit roles (role="table", rowgroup, row, columnheader, rowheader, cell) and test with VoiceOver and NVDA.

  • ▸

    WCAG 2.2 success criterion 1.3.1 Info and Relationships requires that relationships shown visually (like which header labels a cell) are also available in code. th, scope, headers and caption are how a table meets it. Criterion 1.4.10 Reflow allows two-direction scrolling for content like data tables, which is why a scroll wrapper is an acceptable responsive strategy.

  • ▸

    For very large data (thousands of rows), keep the real <table> but render only the visible rows (virtualisation). ARIA aria-rowcount and aria-rowindex then tell screen readers the full size and each row's true position.

Remember this

  1. 1

    For that to work, the reader must know which header belongs to which cell. In a simple table with one header row on top and maybe one header column on the left, browsers usually guess correctly. The scope attribute removes the guesswork: <th scope="col"> labels the cells below it, <th scope="row"> labels the cells to its right. scope="colgroup" and scope="rowgroup" label a whole group of columns or rows, for a header that spans them.

  2. 2

    Some tables are too tangled for scope: headers in the middle of the body, several header levels, or cells that belong to headers that don't line up. For those, give every th an id, and give each data cell a headers attribute listing the ids of all its headers, separated by spaces: <td headers="q1 north">. This states the link exactly, cell by cell. Often a better fix is to split one complicated table into two simple ones.

  3. 3

    Never use a table for page layout (putting a sidebar in one cell and the content in another). That was common before CSS layout existed. A screen reader announces it as a table with rows and columns that mean nothing, the reading order is fixed by the cells, and the layout can't adapt to small screens. Use CSS Grid and Flexbox (both in the CSS course, /css). If you inherit an old layout table, role="presentation" on the <table> tells assistive technology to ignore its table meaning.

  4. 4

    The opposite mistake is building a data table out of divs styled with CSS Grid. It looks like a table but has no rows, columns or headers for a screen reader, and it can't be copied into a spreadsheet properly. If the data is tabular, use <table>: the meaning is free.

  5. 5

    Wide tables on narrow screens need a plan. The simplest and most robust: wrap the table in a div with overflow-x: auto, so the table scrolls sideways inside the box instead of making the whole page scroll. Make that box usable by keyboard and screen reader: tabindex="0" (so keyboard users can focus it and scroll with arrow keys), role="region" and an accessible name with aria-labelledby pointing at the caption.

  6. 6

    Other strategies: hide less important columns on small screens, make the first column or header row sticky (position: sticky, in the CSS course), or turn each row into a stacked "card" with CSS, showing each value with its column name. The card approach changes the display of table elements, and some browsers then drop the table meaning, so test it with a screen reader, and add ARIA roles (role="table", role="row", role="cell") back if needed.

Explain it without notes

01

How does a screen reader know which headers to read for a data cell, and how do scope and headers help?

02

When would you choose headers/id over scope?

03

Why shouldn't tables be used for page layout, and what should you use instead?

04

What are three ways to make a wide table usable on a phone, and what does each cost?

05

A designer built a price comparison from divs with CSS Grid. What would you tell them?

Practice

01

Add correct scope values to a table with a header row (Plan, Price, Data) and row headers for three plans.

02

Write a two-level header table: Morning and Evening each span two columns (In, Out), with one row for Mon. Give it scope="colgroup" and scope="col" correctly.

03

Wrap any wide table in an accessible scroll container.

Trade-offs

  • ↔

    headers/id is precise for any table but verbose and easy to break when rows change; scope is short and robust but only fits regular grids.

  • ↔

    A scroll wrapper keeps the table's meaning intact but hides columns off-screen; stacked cards avoid sideways scrolling but make comparing rows harder and need screen reader testing.

Done when you can

  • I can add scope to row and column headers, including colgroup and rowgroup.

  • I can link cells to headers with headers and id in a complex table.

  • I can explain why tables are for data, not layout.

  • I can make a wide table scroll inside an accessible, focusable region.

  • I know the trade-offs of scrolling, hiding columns and stacking rows on small screens.