Data Display
Table
AvailableTabular data. Properly associating the table element with header cells is key for screen reader announcement.
What Is a Table?
A table is a UI for presenting static data organized into rows and columns. It is used for pricing comparisons, sales summaries, spec lists, schedules, and anywhere you need to display values or items labeled by row and column headings.
What we cover here is a table for reading, not an interactive widget. If you need cells that can be edited or navigated with arrow keys, use theinteractive data grid pattern (grid) instead. For a regular table, writing correct semantic <table> HTML gives you most of the accessibility you need for free.
Why Does Accessibility Matter?
Sighted users can instantly tell which row and column a value belongs to by glancing at the headings above or to the left. The key is delivering that cell-to-heading relationship to people who cannot see the layout.
- Screen reader users: With a proper
<table>, moving to a cell announces something like "Shibuya, Q2, 148" —row heading, column heading, and cell value together. This lets users navigate horizontally and vertically without getting lost. - On the other hand, laying out
<div>elements to look like a table makes the data sound like a flat list of numbers to assistive technology. "120" conveys nothing about which store or which quarter it represents.
The fix is simple: use <table> + <caption> +<th scope>. That alone establishes the associations.
Live Demo (Recommended Implementation)
Below is a semantic table with properly assigned column headings (scope="col") and row headings (scope="row"). It looks like an ordinary table, but the way assistive technology reads it is completely different.
| Store | Q1 | Q2 | Q3 |
|---|---|---|---|
| Shibuya | 120 | 148 | 165 |
| Umeda | 98 | 110 | 132 |
| Hakata | 76 | 84 | 101 |
Try it: Use your screen reader's table navigation (e.g., ⌃⌥ + arrow keys in VoiceOver) to move between cells. Each cell is announced together with its row and column headings.
Tip
Turn on a screen reader (on macOS, press ⌘+F5 for VoiceOver) and navigate to the "165" cell. You will hear something like "Shibuya, Q3, 165" —row heading, column heading, and value announced together. This is the effect of associating headings with <th scope>.
Keyboard Interaction
A static table is not an interactive widget. It has no tab stops, and you do not need to implement arrow key handling (roving tabindex). The commands listed below are table navigation features provided by screen readers in browse (reading) mode — they are assistive technology features, not something you implement.
| Key | Action | Provided by |
|---|---|---|
| ⌃⌥ + ← → ↑ ↓ | Move one cell at a time within the table (VoiceOver table navigation). Announces headings and value at each cell. | Assistive technology |
| Enter table navigation (VO: ⌃⌥⌘+T, etc.) | Enter table reading/navigation mode (the exact key varies by screen reader). | Assistive technology |
| Tab | The table itself is not a tab stop. If the table contains links or buttons, Tab moves to those. | Browser default |
Note
The specific keys differ across screen readers such as NVDA, JAWS, and VoiceOver. The important point is that as long as you use correct <table> markup, these table navigation features become automatically available. You do not need to write any key handling yourself.
Required WAI-ARIA Roles
When you use the native <table> element,ARIA roles are mostly unnecessary — the elements carry the correct roles implicitly. What matters is choosing the right HTML elements.
| Target | Attribute / Role | Meaning |
|---|---|---|
| Entire table | <table> | Conveys that the element is a table. role="table" is implied automatically so it does not need to be set explicitly. |
| Table heading | <caption> | Gives the table a name (title). Screen readers announce it as "table: [caption]" and it appears in table listings. |
| Column header cell | <th scope="col"> | Identifies the cell as a column header. It is associated with all cells in the same column. |
| Row header cell | <th scope="row"> | Identifies the cell as a row header. It is associated with all cells in the same row. |
| Header / body | <thead> / <tbody> | Structurally separates header rows from data rows. |
| Data cell | <td> | Contains the actual data values. Distinguishing <td> from <th> is prerequisite for proper header association. |
Note
For complex tables (e.g., multi-level headings), you can use <th id> and<td headers> to create explicit associations. However, start by writing simple tables correctly with scope. The simpler the table, the more accessible it is.
Recommended Pattern (Good)
Good / Recommended
Use <table> + <caption> + <thead>/<tbody>, with scope="col" on column headings and scope="row" on row headings.
Markup (no JS required — just use the right elements):
<table>
<caption>Quarterly Sales by Store (in thousands)</caption>
<thead>
<tr>
<th scope="col">Store</th>
<th scope="col">Q1</th>
<th scope="col">Q2</th>
<th scope="col">Q3</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Shibuya</th>
<td>120</td>
<td>148</td>
<td>165</td>
</tr>
<tr>
<th scope="row">Umeda</th>
<td>98</td>
<td>110</td>
<td>132</td>
</tr>
<tr>
<th scope="row">Hakata</th>
<td>76</td>
<td>84</td>
<td>101</td>
</tr>
</tbody>
</table>Note
Notice that the top-left "Store" cell is also a <th scope="col">. It serves as the column label for the store names (row headings) below it. Each store name in the rows uses <th scope="row">, making it the heading for all data cells in that row.
Anti-pattern (Bad)
Below is a layout that uses <div> elements with CSS Grid to create a "visual-only table."It looks like a table visually, but to assistive technology it sounds like a flat list of numbers — with no indication of which store or quarter each number belongs to.
Try it: With a screen reader, the content reads as 'Store Q1 Q2 Q3 Shibuya 120 148 165 …' in a flat stream — the row and column heading associations are completely lost.
<!-- ❌ Anti-pattern -->
<!-- Looks like a table but lacks <table>/<th>/scope, so meaning is lost -->
<div class="grid">
<div class="cell head">Store</div>
<div class="cell head">Q1</div>
<div class="cell head">Q2</div>
<div class="cell head">Q3</div>
<div class="cell">Shibuya</div>
<div class="cell">120</div>
<div class="cell">148</div>
<div class="cell">165</div>
<div class="cell">Umeda</div>
<div class="cell">98</div>
<div class="cell">110</div>
<div class="cell">132</div>
</div>Bad / Avoid
Problems with this implementation:
- Headings are not associated with cells — Without
<th>/scope, there is no way to tell that "120" belongs to Shibuya's Q1. - No table name — Without
<caption>, the purpose of the table is unclear. - Table navigation does not work — Without
<table>, screen reader table navigation commands are unavailable. - Sounds like a flat list of numbers — Without structure, the content is read top to bottom with no way to reconstruct meaning.
Tip
If you absolutely must use <div> elements, you need to add role="table", role="row",role="columnheader", role="rowheader", androle="cell" all manually. Using <table> from the start gives you most of that for free.
Implementation Checklist
- Data tables use native <table> elements (not a grid of <div> elements)
- Table has a name (title) via <caption>
- Column header cells use <th scope="col">
- Row header cells use <th scope="row">
- Header rows are wrapped in <thead> and data rows in <tbody>
- <table> is not used for layout purposes (tables are for data only)
- If interactive cell-by-cell navigation is needed, use the grid pattern instead
Source (English):Table Pattern — W3C APG(opens in a new tab)