A screen reader can only tell you which header a cell belongs to if the markup says so. That is most of what makes a table accessible. The th header cell with a scope attribute does the job, and id and headers attributes take over when the structure gets complicated.
WCAG 2.2 has no table-specific success criterion. Tables fall under success criterion 1.3.1 (Level A), and W3C’s Understanding document for it, updated March 2026, handles them through sufficient techniques such as H51, H63 and H43. Failures such as F46 for layout tables are covered there too.
What Are Accessible Tables?
Sighted readers scan across and down without thinking about it.
A screen reader user gets the same orientation only when the markup marks which cells are headers and which cells those headers govern. Then the screen reader can announce the right header for each data cell as you move around.
In practice that means table, tr, th and td for the grid, scope on header cells (or id and headers pairs once the structure gets complex), and a caption element that names the table.
The W3C WAI Tables Tutorial (updated February 2023) sets the baseline. Header cells go in th, data cells go in td, and complex tables may need explicit scope, id or headers associations.
None of this is exotic. It is plain HTML markup, defined by the table content model in the HTML Living Standard (WHATWG), and no script is involved.
Tables are a bit unusual in web accessibility work, because the fix is almost always markup and almost never styling.
Most sites still get it wrong. WebAIM’s 2021 Million analysis counted 1,533,402 tables across one million home pages, and only 140,793 (9.2%) had valid data table markup. The figure dates from 2021 and covers every table, layout tables included.
A div grid styled to look like a table doesn’t count. Neither does a screenshot of a table, or a table whose only job is positioning things on the page.
Data Tables Versus Layout Tables
Use a table when the information has relationships in two dimensions. If you only want things side by side, use CSS.
Rows and columns holding related values make a data table, and that table needs th headers, a caption, and either scope or id and headers pairs. A layout table only positions content, so it should carry no th, no caption and no non-empty summary.
WCAG technique F46 treats th, caption or a non-empty summary inside a layout table as a failure of success criterion 1.3.1. The reasoning is simple enough. Structural markup is being used for presentation, and a screen reader will announce relationships that don’t exist.
Sources disagree on whether layout tables are ever acceptable. WCAG 2 doesn’t prohibit them, the WAI Tables Tutorial says tables are not meant for layout, and UNCG’s guidance allows a correctly built one while noting that best practice discourages it.
Skip the debate. Layouts built with CSS never raise the question, and F46 itself recommends them.
Legacy pages sometimes can’t be rebuilt yet. In that case the ARIA role=”presentation” attribute strips the table semantics, so a screen reader reads the cell content without announcing any table structure.
A one-column table is a list. A table nobody reads across is usually a list too.
How to Mark Up Table Headers
Every header cell gets th and a scope, col for a column header and row for a row header.
Technique H51 (updated 12 January 2026) names table, tr, th and td as the minimum for tabular information. Information counts as tabular when its relationships run in two dimensions.
The sources part ways a little here. WebAIM says all th elements should generally have a scope, because assigning one removes any guessing about direction. WCAG technique H63 says th alone is sufficient for simple tables with headers in the first row or column.
Both work. Scope costs nothing, so I’d add it every time.
For a simple table the build order goes like this.
- Open the table and add a caption as its first child.
- Write the first row with th scope=”col” cells, none of them empty.
- Start each body row with a th scope=”row” cell when rows carry labels.
- Fill the remaining cells with td.
- Check that every row has as many cells as there are column headers.
According to WebAIM, thead, tbody and tfoot add no accessibility behavior. They repeat header rows on printed pages and help with styling, and using tbody does no harm.
Column headers
Put scope=”col” on every th in the top row. Each one governs the cells beneath it.
WebAIM adds a rule that catches a lot of templates. Header cells are never empty, and that includes the top-left corner cell.
Row headers
Technique H51 describes what this sounds like in a schedule table. Moving left or right along a row, the screen reader speaks the day (the column header) followed by the appointment. Moving up or down a column, it speaks the time interval (the row header).
That second announcement only exists because the first cell of each row is a th with scope=”row”. Without it, the user hears bare values.
colgroup and rowgroup scope
A th that spans several columns takes scope=”colgroup”. One that spans several rows takes scope=”rowgroup”. Pair them with colspan and rowspan, and remember that H63 lists row, col, rowgroup and colgroup as the only valid scope values.
Table Captions and Descriptions
Give a data table a caption element as its first child, and keep the text short, closer to a heading than a sentence.
WebAIM calls a caption generally helpful rather than mandatory, and the WAI tutorial calls it useful in most situations. Most screen readers announce caption content, and in Tables Mode the caption is the primary way users identify a table, per the WAI Caption and Summary page.
A heading above the table isn’t the same thing. The caption element is what that navigation mode relies on.
Keep each caption unique on the page. BOIA advises against reusing one caption for two tables, even when the data sets look alike.
Visible beats hidden in most designs. A visually hidden caption names the table for screen reader users only, and sighted users get nothing.
Where a longer description goes is a fair question. It can sit as a second line inside the caption element, visible to everyone, or aria-describedby can point the table at a paragraph holding the summary. A figure with figcaption works too, but Tables Mode users usually can’t identify the table by a figcaption, and combining aria-labelledby with aria-describedby can make the text read twice.
The old summary attribute is obsolete in HTML5, per the note on WCAG technique H73. WebAIM’s data tables page (last updated September 2017) doesn’t recommend it either, and says a table that needs a structural explanation should probably be simplified instead.
The WAI tutorial takes the other side of that argument. A summary is usually needed for complex tables, and it shouldn’t repeat the caption.
Complex Tables: Using id and headers Attributes
Start with scope. Move to id and headers only when a data cell needs header cells that scope can’t reach, and split the table if the structure allows it.
| Approach | Use when | Failure mode |
|---|---|---|
| scope col and row | One header level per direction | Header applies to cells it should not |
| scope colgroup and rowgroup | A header spans several columns or rows | Gaps in some screen readers (WebAIM) |
| id and headers | Column headers repeat or change mid-table | More markup, more ways to break |
| Separate tables | Header levels can stand alone | Comparison across halves is lost |
When scope is enough
The WAI tutorial sorts tables into four tiers. There are tables with one header, tables with two headers (scope col and row), irregular headers (colgroup and rowgroup), and multi-level headers (id and headers).
WebAIM goes further and says scope handles most tables, spanned headers included, because the scope of a header applies to every cell it spans.
Scope first. It’s shorter, easier to maintain, and harder to break.
When id and headers are required
Every th gets a unique id. Every td then gets a headers attribute listing those ids, separated by spaces, with no commas, in the order a screen reader should read them.
The WAI tutorial names the clearest trigger, which is column headers that repeat or change part-way down the table. Technique H43 states it more broadly, for any data cell associated with more than one row or column header.
The sources conflict on how much this method is worth. WebAIM calls it rarely necessary and warns that the result can be technically accessible yet hard for a screen reader user to follow when several header levels are read aloud. A January 2021 thread on the W3C WAI interest group list also notes that the tutorial’s own two-tier header example uses neither id nor headers, even though each data cell relates to three headers.
The upside is explicit association with no guessing, and it copes with headers that repeat or change mid-table. The downside is a lot more markup with more ways to break (WebAIM), long spoken output when three headers attach to one cell, and the fact that each id must be unique across the whole document.
Merged cells
NC State, UNCG and Syracuse University guidance all say not to split or merge cells.
The WAI tutorial goes the other way and documents irregular headers with colgroup and rowgroup, and WebAIM says scope covers spanned headers. The catch, also from WebAIM, is that some screen readers still don’t fully support spanned or multi-level headers, so flatten the table when you can.
Merge only when the data itself has grouped columns or rows. When you do, pair colspan or rowspan with scope colgroup or rowgroup.
Splitting a complex table
University of Colorado Boulder’s web guidance says tables with two or more logical levels of row or column headers should become two simpler tables.
Each piece then needs only scope, and each gets its own caption, which has to be unique.
Split when the top and bottom halves use different column headers. Keep the table whole when readers compare values across the groups.
How Do Screen Readers Read Accessible Tables?
A screen reader announces the table first, with its size, and only then speaks cells one at a time. NVDA, JAWS and VoiceOver each have their own keys for jumping to a table and stepping through it.
Announcement order differs by product. In Adrian Roselli’s 2021 comparisons, JAWS 2020 with Chrome 89 said “table with 5 columns and 10 rows”, while NVDA 2020.4 with Firefox 87 gave the rows first and the columns second.
| Screen reader | Next table | Move between cells |
|---|---|---|
| NVDA | T in browse mode | Control+Alt+arrow keys |
| JAWS | T | Control+Alt+arrow keys |
| VoiceOver (macOS) | VO+Command+T | VO+arrow keys |
Commands come from the NV Access NVDA 2026.2 user guide, Freedom Scientific’s JAWS keystroke documentation and Apple’s VoiceOver guide. VO is Control+Option.
JAWS announces when you enter and leave a table, per Freedom Scientific’s training lesson, and Insert+Ctrl+T lists every table on the page (Penn State accessibility guidance). On the VoiceOver side, Apple documents VO+C for the column header, VO+R for the row header and VO+Shift+T+T for the table dimensions.
VoiceOver can also read header text while you move along a row, but only if that announcement is switched on in VoiceOver Utility. Apple adds that blank columns are skipped during arrow navigation unless table verbosity is set to High.
Cells are spoken in source order, row by row. CSS can move cells around visually, but the DOM sequence decides what gets read.
Making Accessible Tables Responsive
Put the table inside a scrollable region that has an accessible name and can take keyboard focus. The table keeps its semantics, and the page stops scrolling in two directions.
WCAG 1.4.10 Reflow (Level AA) asks for content to work without two-dimensional scrolling at a width equivalent to 320 CSS pixels. Data tables get an exception, but W3C’s Understanding document (updated September 2026) applies it to the table alone.
That 320-pixel width is what a 1280-pixel viewport becomes at 400% zoom. Test at that setting.
The wrapper needs role=”region” with aria-labelledby pointing at the caption’s id. Add tabindex=”0″ so keyboard users can focus and scroll it, and put overflow: auto in the CSS.
Roselli’s minimal version of the pattern maps those pieces to WCAG 2.1.1 Keyboard, 4.1.2 Name, Role, Value and 1.4.10 Reflow. J.P. Morgan’s Salt design system applies the same idea, wrapping tables in a scrollable region once the viewport drops below set breakpoints.
W3C’s own failing example shows why the wrapper matters. A table placed directly in the page pushed the paragraphs below it into two-dimensional scrolling, and that fails even though tables are exempt.
The same logic reaches the neighbors. A heading, search field or pagination control around a table isn’t exempt, so each one has to fit the narrow viewport.
Stacked-card layouts are popular in responsive design, and they carry a cost. Restyling table elements with CSS display values can break table semantics in some browsers (Roselli, November 2020), and ARIA has no attribute that maps to the headers attribute for spanned cells.
So a scrollable wrapper keeps real table semantics and costs sighted users some sideways scrolling. A stacked layout avoids the scrolling and puts the semantics at risk. I lean toward the wrapper.
Sizing matters too. WebAIM advises percentage widths over pixel widths, and no fixed cell heights, so cells can grow when users enlarge text.
Which WCAG Criteria Cover Accessible Tables?
WCAG 2.2 has no table-specific success criterion. The work is spread across 1.3.1, 1.3.2, 1.4.10, 2.1.1 and 4.1.2.
1.3.1 Info and Relationships is the main one, since header-to-cell relationships must be programmatically determinable. H51, H39, H63 and H43 are listed as sufficient techniques, and the documented failures include F48 (a pre element used for tabular data), F90 (headers and id associated incorrectly) and F91 (headers not marked up correctly).
F92 guards role=”presentation”. It fails any content that carries real semantics, so a data table can never take it.
1.3.2 Meaningful Sequence treats tables as meaningful sequences, so source order has to read correctly. A layout table that stops making sense when linearized fails here (F49).
The remaining three, 1.4.10, 2.1.1 and 4.1.2, govern the scrollable wrapper. They cover reflow, keyboard focus and an accessible name.
Read together, the criteria and techniques demand less than most checklists do. 1.3.1 asks that relationships be programmatically determinable, H63 calls th alone enough for simple tables, and WebAIM still asks for scope on every th. Choosing scope is a quality decision, not a conformance requirement, until a table matches one of the named failures.
On levels (WCAG 2.2, W3C), 1.3.1, 1.3.2, 2.1.1 and 4.1.2 are all Level A. 1.4.10 Reflow is Level AA, with a threshold of 320 CSS pixels for vertically scrolling content.
How to Test Accessible Tables
Run an automated scan for structural errors, then read the table with a screen reader and zoom to 400%. The scan catches broken markup. It can’t tell you whether each cell announces the right header.
axe DevTools (Deque) has td-headers-attr and th-has-data-cells rules that carry WCAG 1.3.1 tags, while scope-attr-valid and table-duplicate-name are best-practice rules. Lighthouse flags headers attributes that point outside the table, th elements with no data cells, and captions faked with a colspan row. WAVE reports an empty table header as an error.
Each Lighthouse audit is pass or fail, so one broken table fails the audit for the whole page. Chrome’s documentation weights the large-table header audit at 10 and the other three table audits at 7 each.
Deque’s 2021 study found its automated testing covered 57% of accessibility issues by volume. The commonly cited 20 to 30% counts coverage by success criteria instead, so the two numbers measure different things.
Neither figure means a scanner can judge a header assignment. That check stays manual.
- Tab to the table. If it scrolls, the wrapper takes focus and the arrow keys scroll it.
- Read three cells from different corners with NVDA, JAWS or VoiceOver and confirm each announces its headers.
- Zoom to 400% at a 1280-pixel width and look for page-level sideways scrolling.
- Compare the spoken order with the visual order.
A few failures show up again and again. A caption faked with a colspan row gets flagged by Lighthouse, and the fix is a real caption element. A headers value with no matching id fails td-headers-attr, so correct the id or remove the attribute. A th with no data cells fails th-has-data-cells and should become a td.
Add these table checks to a broader web accessibility checklist so they run on every release.
When Accessible Table Markup Fails
Correct markup can still fail. Sometimes the browser or screen reader mishandles it, sometimes the wrapper makes announcements long, and sometimes the content never needed a table.
Browser and screen reader bugs
Android 9, 10 and 11 with Chrome and TalkBack skipped a non-interactive table placed inside a tabindex container, whatever the input method, according to Adrian Roselli’s December 2020 report.
A March 2021 update said the fix would ship in Chrome 91. Older Android builds still need a real-device test.
Verbose announcements from scroll wrappers
Salt (J.P. Morgan) points aria-labelledby at the table itself. That guarantees a name, but some browser and screen reader pairs then read the table name and its content when focus enters the region. Control stops the speech in NVDA and JAWS.
Roselli points aria-labelledby at the caption instead and leaves any mention of scrolling out, to keep announcements short. In his tests NVDA 2020.4 appeared to skip a region name identical to the table’s.
Both are defensible. Salt trades verbosity for a guaranteed name.
When a table is the wrong format
Some data doesn’t belong in a table, however well it’s marked up.
The University of New Hampshire’s guidance suggests asking whether a long table that breaks across pages can become simpler tables, and adding a written description beside content-heavy ones.
Put the single takeaway in a sentence next to the table. Split by topic when rows stop sharing columns. For datasets people will filter or sort themselves, a CSV file is the better offer.
For large datasets, converting the HTML table to CSV gives people a file they can open in their own tools.
Accessible Tables FAQ
How do you make a sortable table accessible?
Set aria-sort on the header of the currently sorted column, with the value ascending or descending, and wrap sortable header text in a button element.
The W3C ARIA Authoring Practices Guide example removes aria-sort from the old column when the sort changes. It also adds an off-screen sort description to the caption instead of repeating it on every button.
Should data tables use ARIA grid roles?
No. ARIA grids are interactive widgets, not data tables, according to Adrian Roselli, so read-only data belongs in a plain HTML table.
The grid pattern fits tables where users edit cells, sort or hide columns.
Can a div with role=”table” replace the table element?
It can work, but the native element is safer. The APG publishes a table example built from div and span elements, and it also advises maximizing semantic HTML.
ARIA can’t match everything. No attribute maps to the headers attribute for spanned cells (Roselli).
Is a screenshot of a table accessible?
No. An image of a table exposes no header relationships to a screen reader.
WCAG 1.3.1 asks for relationships that software can determine or that appear in text. Real table markup is the fix, and a text description is the weaker fallback.
What should go in empty table cells?
Screen reader-only text, rather than nothing. Washington State University’s guidance recommends it especially in complex tables, where one cell answers to several headers.
Apple adds that VoiceOver skips blank columns during arrow navigation unless table verbosity is set to High.
Do Section 508 and EN 301 549 require accessible tables?
Yes, through WCAG. The Access Board’s 2017 Section 508 Refresh incorporates WCAG 2.0 Levels A and AA, with compliance required from January 2018.
EN 301 549 clause 9 references WCAG to Level AA. The version currently in legal effect, V3.2.1, points to WCAG 2.1, while V4.1.1, published on 2 September 2026, moves to WCAG 2.2 Level AA but takes legal effect only once it is cited in the Official Journal of the EU. Success criterion 1.3.1 is where tables are tested.
Can color alone mark table rows or cells?
No. WCAG 1.4.1 (Level A) bars color as the only visual means of conveying information, so a color-coded status cell needs text or an icon beside it.
Colors that differ in lightness count as an extra cue only at a contrast ratio of 3:1 or higher.
How do you build accessible tables in a CMS or document editor?
Switch on the header row option, add a caption, and run the built-in accessibility checker.
The W3C WAI tutorial notes that many authoring tools and CMS platforms define header cells during table creation. It also warns that table markup is often lost when content converts between formats.
Keeping New Tables Accessible After Launch
Tables stay accessible when the editor, the publishing check and the review schedule all enforce the same header and caption rules.
Order the controls by what a failure costs. Make the header row and caption required at insertion, run an automated scan on every publish, and keep a screen reader review for the high-traffic tables.
Prevention comes first because Lighthouse audits are pass or fail, so one broken table fails the audit for the whole page, while Deque’s 57% coverage leaves 43% of issue volume without full automated coverage.
The trade-off is editor friction, since required fields slow every pasted table. Accept it, because the later steps only catch what the insert step failed to prevent.
Teams publishing through WordPress can apply the same rules while adding tables in WordPress, starting at the insert step.
- What is Backend in Web Development? - September 13, 2026
- What is Frontend Development? - September 11, 2026
- How to Make a Button in Figma: Design Best Practices - September 10, 2026


