Accessible Tables: scope, headers and id
A table a screen reader can read is a table with named headers. scope handles simple tables, headers and id handle the hard ones.
-
HTML Basics
- What is HTML: The Structure Layer of Every Web Page
- HTML Document Structure: DOCTYPE, html, head and body
- Elements, Tags and Attributes: The Vocabulary of HTML
- HTML Comments: Notes That Ship With Your Code
- Block Level and Inline Elements
- Writing and Running Your First HTML Page
- How a Browser Turns Markup Into a Page
- Text and Formatting
- Links and Navigation
- Images and Media
- Lists
- Tables
-
Forms
- Form Structure: form, action and method
- Input Types: Text, Email, Number, Date and the Rest
- Labels: The Most Important Element in a Form
- Checkboxes, Radio Buttons and Grouping
- select, option, optgroup and datalist
- textarea, File Uploads and Hidden Fields
- Buttons: submit, reset and button
- Built In Form Validation
- GET or POST: What Happens When a Form Is Submitted
- Semantic HTML
- HTML5 Features
- Head and Metadata
- HTML with CSS
- HTML with JavaScript
- Accessibility
-
HTML SEO
- How Google Works: Crawling, Indexing and Ranking
- SEO Friendly HTML Structure
- Titles and Descriptions That Earn Clicks
- Headings and Content Structure for Search
- Internal Linking and Anchor Text
- robots.txt and XML Sitemaps
- Canonical URLs and Duplicate Content
- Structured Data and JSON-LD
- Image SEO
- Core Web Vitals and Mobile Friendliness
- DevTools and Debugging
- Editor Productivity
- HTML Best Practices
- HTML Projects
- Advanced Projects
- Practice and Exams
Concept
Someone looking at a table reads a cell and glances up and left to see what it means. A screen reader user cannot glance. The browser has to be told which header applies to which cell, and it announces those headers every time the reader enters a new cell.
Marking a cell as th is the first half. Declaring what that header covers is the second, and there are two mechanisms:
scope- for simple tables. Says which direction a header applies in.headersandid- for complex tables. Names exactly which headers apply to each cell.
scope
| Value | Meaning |
|---|---|
col | Labels every cell below it in this column |
row | Labels every cell to its right in this row |
colgroup | Labels the columns it spans |
rowgroup | Labels the rows it spans |
<table>
<caption>Fee structure by course and year</caption>
<thead>
<tr>
<td></td>
<th scope="col">Year 1</th>
<th scope="col">Year 2</th>
<th scope="col">Year 3</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Design</th>
<td>48,000</td><td>52,000</td><td>56,000</td>
</tr>
<tr>
<th scope="row">Data science</th>
<td>54,000</td><td>58,000</td><td>62,000</td>
</tr>
</tbody>
</table>Moving to the cell holding 58,000, a screen reader announces Data science, Year 2, 58,000. Without scope it may still guess correctly in a simple table, but the guess is the browser heuristic and it fails as soon as the table gets interesting. Declare it.
headers and id
When a cell is governed by headers that scope cannot describe - two header rows, headers in the middle of the table, irregular spans - each header gets an id and each data cell lists the ids that apply to it.
<table>
<caption>Marks by subject and assessment</caption>
<thead>
<tr>
<td></td>
<th id="sem1" colspan="2">Semester 1</th>
<th id="sem2" colspan="2">Semester 2</th>
</tr>
<tr>
<td></td>
<th id="s1th" headers="sem1">Theory</th>
<th id="s1pr" headers="sem1">Practical</th>
<th id="s2th" headers="sem2">Theory</th>
<th id="s2pr" headers="sem2">Practical</th>
</tr>
</thead>
<tbody>
<tr>
<th id="design">Design</th>
<td headers="design sem1 s1th">72</td>
<td headers="design sem1 s1pr">81</td>
<td headers="design sem2 s2th">75</td>
<td headers="design sem2 s2pr">88</td>
</tr>
</tbody>
</table>The cell holding 81 is now announced as Design, Semester 1, Practical, 81. The headers attribute takes a space separated list of ids, and the order you write them is the order they are read.
It is verbose and it must be maintained by hand, which is exactly why it is a last resort rather than a default.
Which to use
| Table | Use |
|---|---|
| One header row | scope="col" |
| Header row and header column | scope="col" and scope="row" |
| Spanning headers, one level | Add scope="colgroup" and scope="rowgroup" |
| Two header levels or irregular structure | headers and id |
| Genuinely complicated | Split it into two tables |
Other things a table needs
A caption
The accessible name of the table. Always present.
A summary for complex tables
The old summary attribute is obsolete. Describe the structure in a paragraph before the table, or inside the caption:
<caption>
Marks by subject and assessment
<span class="caption-note">Four columns, grouped into two semesters,
each showing theory and practical marks.</span>
</caption>Do not leave header cells empty
The corner cell of a cross tabulated table is the exception and should be a td. Any other empty th leaves a column with no name.
Keep a scrolling table keyboard reachable
A table in a horizontally scrolling container must be scrollable by keyboard, which means the container needs a tab stop and a name:
<div class="table-scroll" tabindex="0" role="region" aria-label="Fee structure">
<table>...</table>
</div>Important rules
- Every id must be unique in the document.
- A cell may reference several headers; separate the ids with spaces.
- Do not mix
scopeandheaderson the same table. Pick one. - The
summaryattribute is obsolete. Do not use it. role="presentation"on a table removes its table semantics entirely, which is only correct for a legacy layout table you cannot rewrite.
Common mistakes
- Relying on the browser to guess headers rather than declaring
scope. - Using
headerswith an id that does not exist, or that is misspelt. - Applying
headersto some cells and not others. - Leaving a header cell empty.
- Omitting the caption.
- Building a table so complex that no markup can rescue it.
Best practices
- Caption and
scopeon every table. That covers the overwhelming majority of real tables. - Reach for
headersandidonly when the structure genuinely demands it. - Prefer splitting a complex table over annotating it.
- Test with a screen reader by moving cell to cell and listening to what is announced.
- Wrap wide tables in a labelled, focusable scroll container.
Practice
- Add
scopeto a table with headers on both axes, then listen to a middle cell with a screen reader. - Build a two level header table using
headersandid, and confirm each data cell announces all three headers. - Take the same table and split it into two simple ones. Compare how they read.
- Wrap a wide table in a scroll container that a keyboard can reach and scroll.