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
ththat 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
regionorpresentation. - 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.
<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.
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.
<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.
<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.
<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
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
Follow another cell's headers
In the second example, move
id="target"to the135cell. Predict the sentence before you run: which region, product and month should it say? - 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
See the cards
Make the preview narrower than 480px (or change
max-width: 480pxtomax-width: 2000px). Each loan turns into a card with labels likeDue: 16 Oct.
Code & diagrams
`abbr` gives a long header a short form that some screen readers use when repeating it for every cell.
Preview
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: EngThe 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
Console
Nothing logged yet.Expected console output
The highlighted cell is read as: South, Coffee, Jan, 60Make 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
Console
Nothing logged yet.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
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".
Break #2
Build the table from divs
Replace the marks table with divs laid out by CSS Grid, styled to look identical.
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
tableis for data or layout (for example: hasth,caption,thead, or borders). Giving data tables real headers and a caption means you never depend on those guesses. - ▸
Setting
display: blockordisplay: gridon 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,headersandcaptionare 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). ARIAaria-rowcountandaria-rowindexthen tell screen readers the full size and each row's true position.
Remember this
- 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
scopeattribute removes the guesswork:<th scope="col">labels the cells below it,<th scope="row">labels the cells to its right.scope="colgroup"andscope="rowgroup"label a whole group of columns or rows, for a header that spans them. - 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 everythanid, and give each data cell aheadersattribute 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
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
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
Wide tables on narrow screens need a plan. The simplest and most robust: wrap the table in a
divwithoverflow-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 witharia-labelledbypointing at the caption. - 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 thedisplayof 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
How does a screen reader know which headers to read for a data cell, and how do scope and headers help?
When would you choose headers/id over scope?
Why shouldn't tables be used for page layout, and what should you use instead?
What are three ways to make a wide table usable on a phone, and what does each cost?
A designer built a price comparison from divs with CSS Grid. What would you tell them?
Practice
Add correct scope values to a table with a header row (Plan, Price, Data) and row headers for three plans.
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.
Wrap any wide table in an accessible scroll container.
Trade-offs
- ↔
headers/idis precise for any table but verbose and easy to break when rows change;scopeis 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
scopeto row and column headers, includingcolgroupandrowgroup.I can link cells to headers with
headersandidin 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.