On this page
A data table is useful because readers can compare values across two dimensions. Accessibility work should preserve that relationship, not simply make the component fit a narrow screen. A visually tidy collection of cells can still be incomprehensible when someone navigates by row, enlarges text, or listens to one value at a time.
Start with the relationships people need
Write down what each row represents, what each column measures, and which information identifies a record. For an invoice table, that might be invoice number, customer, due date, and amount. A user hearing “£240” needs enough context to know whose invoice it describes and what the amount means.
Use the contrast checker to compare foreground and background values for header text, body text, and interactive controls. Check selected and hover states separately. Readable cells are necessary, but contrast cannot establish the relationships between them; those come from the markup.
Choose a table for genuinely tabular information, not for arranging a form or aligning a page. The WAI tables tutorial offers examples of simple and complex relationships. Start with the simplest structure that faithfully represents the data, and split unrelated datasets rather than forcing everything into one enormous matrix.
Give the table a name and real headers
Use native table elements before considering ARIA. A caption identifies the dataset for people scanning visually and for users locating tables through assistive technology. Make it specific: “Outstanding invoices, April 2026” is more useful than “Results.” Put lengthy explanations in adjacent text rather than stuffing every instruction into the caption.
For straightforward tables, use column header cells with scope set to column and row header cells with scope set to row. A row header should identify the record, not merely be whichever cell happens to appear first. The browser can then expose relationships without custom keyboard behavior.
The following markup keeps an invoice identifier associated with its amount and due date:
<table>
<caption>Outstanding invoices, April 2026</caption>
<thead>
<tr>
<th scope="col">Invoice</th>
<th scope="col">Due date</th>
<th scope="col">Amount</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">INV-1042</th>
<td>30 April 2026</td>
<td>£240.00</td>
</tr>
</tbody>
</table>Avoid replacing these elements with generic containers and a large collection of roles. Native semantics already encode the structure. Consult the MDN table reference when deciding where captions, header groups, and body groups belong. Empty header cells should not stand in for missing labels; give action columns a meaningful heading such as “Invoice actions.”
Handle complex headings and table controls deliberately
Grouped columns can need more than a single header per value. First ask whether repeated, explicit headings would make the table easier to understand. Simpler structure benefits everyone, especially when the alternative is several levels of merged cells with abbreviated labels.
Where complex relationships are unavoidable, use unique header IDs and explicit headers attributes on relevant cells. Each data cell should reference every applicable heading. Verify the resulting associations in actual screen readers rather than assuming a visually correct span creates the intended spoken context. Avoid duplicate IDs when multiple table instances appear on a page.
Sorting belongs on a real button inside the relevant header. Expose the active direction through aria-sort on the sorted header cell, and update it when the order changes. Keep focus on the button after sorting. A small arrow alone does not explain either the action or the current direction.
For checkboxes, provide row-specific names such as “Select invoice INV-1042.” For repeated action links, include enough accessible context to distinguish “View invoice INV-1042” from the next row's link. Do not make every cell focusable: ordinary tables are read with table navigation commands, while Tab should reach genuine controls. Our WCAG interface guide provides broader context for names, roles, and keyboard access.
Preserve relationships on narrow screens
A scrolling wrapper is often the least surprising responsive treatment for comparison-heavy data. It lets columns retain their meaning without shrinking text to illegibility. Keep horizontal overflow inside the component so unrelated page content still reflows normally. Data tables can qualify for WCAG's two-dimensional layout exception, but that is not permission to make the entire page scroll sideways.
<p id="invoice-scroll-hint">Scroll horizontally to see all columns.</p>
<div class="table-scroll" role="region"
aria-label="Outstanding invoices"
aria-describedby="invoice-scroll-hint" tabindex="0">
<!-- Insert the complete native table here. -->
</div>.table-scroll {
max-width: 100%;
overflow-x: auto;
}
.table-scroll:focus-visible {
outline: 3px solid #175cd3;
outline-offset: 3px;
}
.table-scroll table {
min-width: 38rem;
border-collapse: collapse;
}Make a wrapper focusable when keyboard users need it to operate overflow, and check browser behavior rather than creating redundant stops everywhere. Give a focusable region a useful name and a visible focus indicator. The hint must match reality; conditionally showing it only when content overflows can reduce noise.
A stacked-card alternative can work for record browsing, but it weakens cross-row comparison. Do not rely solely on CSS-generated labels or assume changing table elements to block layout preserves semantics across all browser combinations. Test your actual implementation. The responsive container guide helps separate page-level sizing decisions from the table's necessary overflow.
Test with realistic content and navigation
Use long customer names, missing values, negative amounts, and translated headings. A placeholder dash needs an understandable meaning, such as “Not available,” not an unexplained visual convention. Check text enlargement and a narrow viewport, including whether sticky headers or columns conceal focused controls.
Run a keyboard pass first: enter the scrolling region, move it horizontally where needed, activate sorting, and reach every row action without getting trapped. Then use a screen reader's table commands to move across a row and down a column. Confirm that the caption, row identifier, and applicable column heading provide useful context.
Check a representative desktop and mobile assistive-technology combination when the table is important to core tasks. Automated audits can identify some structural omissions, but they cannot decide whether a caption explains the dataset or whether a header association makes business sense. The WCAG information and relationships guidance explains the underlying requirement.
Finally, test a refresh, sorting update, and empty result. Stable focus and a clear empty-state explanation matter just as much as the populated table. Document the expected header announcements so future refactors have a concrete acceptance target.
Key takeaways
- Preserve native table structure, with a useful caption and explicit headers.
- Keep sorting and row actions keyboard accessible and clearly named.
- Prefer contained overflow when comparison requires two dimensions.
- Verify header announcements, zoom behavior, and focus with realistic data.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor