A screen reader reads a table one cell at a time, and reads the header cells alongside each one so the listener does not lose track of where they are. That is the whole mechanism, and it only runs when the markup says which cells are headers. So the two things a data table genuinely needs are real table elements and header cells marked as th. Everything else on this page is refinement sitting on top of those two.
You read a table the other way, by tracking up a column and across a row and holding the headers in your head as you go, and you do it without noticing you are doing it. Take the layout away and a cell reading $4,200 is a number with no quarter and no region attached to it. W3C's own worked example is a bus timetable, stops down the side, buses across the top, every cell a time. Read that without headers and it is forty numbers in a row.
Four Things a Data Table Wants, and Only Two Are the Rule
- Real table markup. A
tableelement withtr,th, andtd. Not a grid of positioned divs, and not text pushed into columns with tabs or spaces. - Header cells marked as headers.
thrather thantd, for the top row and for the first column wherever that column labels the row. - Scope on every header.
scope="col"orscope="row", so each data cell resolves to the header that governs it rather than the header nearest to it. - A caption. The
captionelement gives the table a name, and a name is how somebody finds this table in a list of every table on the page.
The first two are what the standard turns on. The other two are good practice that W3C stops short of requiring, and knowing which is which keeps a finding from being thrown out. Captions are not required in every case to meet WCAG. W3C also publishes a small table with one header row and no scope at all as acceptable, because the data in each column is different enough that nothing is ambiguous. Do all four anyway. Just do not write up a missing caption as a failure. Our fix guides carry the detail on table markup for tabular data, scope on headers, and captioning data tables.
Why Scope Is Worth Setting Even Where It Is Not Required
W3C's own reason is not a conformance argument, it is an observed behavior, and it is the more persuasive of the two. When the direction of the th elements is ambiguous, some screen readers read the whole header row into the last cell. A listener in the Venue column hears Date, Event, Venue and then the venue. One attribute per header stops that, and you cannot tell from looking at the table which screen reader is going to need it.
Caption and Summary Are Two Different Things
A caption works like a heading for the table, and most screen readers announce it. A summary is a different job. It explains how a complicated table is organized, so somebody knows what shape they are about to walk into, and it is usually only needed on complex tables. Give a table both and the summary must not repeat what the caption already said, because a listener hears them back to back.
Designers reach for figure and figcaption instead, because it styles more easily and it looks like the same idea. W3C's warning about that is specific. Screen reader users working in tables mode, which is the mode that lists every table on a page so somebody can jump straight to the one they want, usually cannot identify a table by a caption attached that way. Put the caption element inside the table. If you carry the summary in aria-describedby instead, know that it is the less widely supported of the routes.
Complex Tables, and the Attribute Pair to Reach For Last
A header that spans several columns or several rows is still a scope problem, not a headers problem. The spanning header takes scope="colgroup" or scope="rowgroup", and the group it names has to actually exist in the markup, which means colgroup elements for columns and thead, tbody and tfoot for rows. One attribute, no ids to keep in step, nothing to wire up wrongly.
Two shapes of table are genuinely beyond scope, and they are the two W3C names in its tutorial on multi-level table headers. One is a table where the column headers repeat or change part-way down. The other is a table where three or more headers apply to a single cell, which is not the everyday case of one row header and one column header. Those two need the headers and id attributes, where each data cell names the ids of every header that governs it.
It is laborious and it is easy to get wrong, and a mis-wired headers attribute is worse than none at all, because it confidently hands the listener the wrong header instead of leaving them to work it out. Our guide on headers pointing at the wrong ids covers what that failure looks like in practice.
The better answer, usually
W3C reaches the same conclusion before it reaches for the attributes. Where a table is complex enough to need headers and id, it says the information is worth restructuring so the table is less complex for everybody reading it, and it points at splitting one table into smaller, more manageable tables. That is easier to mark up correctly and easier to read, including for the people who found the original hard going.
Layout Tables Are Not Banned, and the Real Failure Runs the Other Way
WCAG does not prohibit using a table to position things on a page. The named failure runs the opposite way. It is putting th elements, a caption, or a non-empty summary into a layout table, because those tell assistive technology that a grid of positioned content is data with relationships inside it, and now a listener is hunting for a meaning nobody put there. A layout table carrying none of the three does not fail, even with no role="presentation" on it.
Add role="presentation" anyway. It is the safer answer, and it is not the line between passing and failing, so do not report its absence as one. It is also the attribute with a trap on the other side of it. Put role="presentation" on a table that does hold data and you have stripped the structure off a real table, which is a failure in its own right and is the most common way we watch somebody break a table while trying to fix it. Read the table before you label it. Our fix guide covers header cells inside a layout table.
On the web, use CSS. That is the whole answer. Layout tables survive in email templates, where the rendering engines are old enough that developers still reach for them, and in old CMS content nobody has touched in a decade.
Tables on Small Screens
This is where accessibility and responsive design collide, and the standard sides with the table. A ten-column table will not fit a 320-pixel screen, and the reflow rule carries an exception for content that needs a two-dimensional layout. Its list of examples names data tables outright, and then adds three words in brackets that people drop. Not individual cells. So the table is allowed to scroll sideways. A single cell whose text refuses to wrap is not covered by that, and neither is the rest of the page, because W3C would rather the two-dimensional scrolling stayed inside the part that needs it than spread to everything.
What a scrolling table needs is to be reachable, and the usual advice here is stricter than the rule in a way that costs your keyboard users a tab stop.
- Make the scrolling region reachable by keyboard. A table with links or form controls in its cells already is, because a focusable child satisfies it. A table of plain text is the one that needs
tabindex="0"on the wrapper. - If you add
tabindex="0", addrole="region"and a name with it. A focusable div with no role and no name is a silent tab stop, which trades one problem for another. - Keep the header row in view while the table scrolls, wherever the layout lets you.
- Do not cut content off.
overflow: hiddenon a table is the pattern W3C names, because the part that scrolled away is not hidden, it is gone. - Do not drop columns on small screens without a way to get them back. Information removed is information lost.
Turning a table into stacked cards with CSS alone is the pattern to be most careful with. It looks right on the phone and the markup underneath has quietly stopped matching it, because display: block on table elements removes the table semantics in most browsers. What a screen reader then meets is something announced as a table whose structure no longer corresponds to anything on screen.
Small Habits That Save a Table
- One topic per table. Stacking two subjects into one grid with extra header rows makes some screen readers read every
thin the column aloud, which is exactly the confusion headers were supposed to prevent. Start a new table when the topic changes. - No
brelements standing in for rows. The alignment holds until somebody resizes the text, and then it collapses. - Real
thelements, nottdcells styled to look like headers. Bold is not a relationship. - Left-align text and right-align numbers in left-to-right languages, so columns of figures line up on the digits.
- Alternate the row colors. Zebra striping is not decoration. It helps people with reading difficulties and people working at enlarged text sizes keep their place across a wide row.
One honest limit
Two automated rules touch tables and both test structure rather than meaning. One checks that a headers attribute points at cells inside the same table. The other checks that a header cell has any cells assigned to it at all. Neither one reads the table. So a scanner will tell you a table has no th elements, and it will not tell you the th elements are on the wrong cells, that your scope values are backwards, or that headers points at the wrong ids. Those need somebody reading the table with a screen reader, cell by cell.