Wcag explained

Tables: the part of SC 1.3.1 that breaks most often

A table only works if the code says it is a table. What header cells, scope and captions do, why a div grid fails, and how to check a table in seconds.

Opening hours. Monday, 09:00 to 17:00. You look at it once and you know which time belongs to which day, because the two sit on the same row.

A screen reader does not see rows. It reads what the code declares. If that table is built from divs with CSS that makes them line up, what reaches the user is a stream of fragments: “Monday.” “09:00 to 17:00.” “Tuesday.” “09:00 to 17:00.” The relationship that made the whole thing readable is gone.

This is SC 1.3.1 Info and Relationships applied to the one component where it goes wrong most often, and where the damage is hardest to work around.

What a table has to do

A data table is a grid of relationships. Every cell belongs to a column and usually to a row, and the value in it is meaningless without both. The markup has to say so:

  • <table> declares that this is a table. A screen reader announces “table, 4 rows, 3 columns” and switches to table navigation mode, where the user can move cell by cell with the arrow keys.
  • <th> declares a header cell. When the user lands on a value, assistive software reads the headers that apply to it first: “Pro, price, 19 euro per month.”
  • scope="col" or scope="row" tells the browser which direction a header governs. Without it, a table with headers on both axes is guesswork.
  • <caption> gives the table a name, which is what a user hears when they jump to it from a list of tables.

Get those four right and the table works. Miss the header cells and the table is a grid of numbers with nothing attached to them.

Why the div version fails so badly

A pricing table built from divs looks identical on screen and is worthless underneath. There is no table role, so no table navigation mode. There are no header cells, so no announcement of what a value means. There is no row structure, so the reading order is whatever the DOM happens to be.

The user can still hear every word on the page. They just cannot reassemble the grid, and the more columns you have the less recoverable it gets. With two columns someone might work it out. With a six-column comparison table they will not.

Search engines have the same problem in a milder form: real table markup is one of the clearer structured-data signals you can give them, and a div grid gives them nothing.

The other direction: layout tables

The opposite mistake is still alive in email templates and older CMS themes. A <table> used to position a logo next to a paragraph has no headers and no relationships, but assistive software still announces “table, 3 rows, 2 columns” and still offers table navigation for something that is not data. It is noise dressed as structure.

If you genuinely cannot avoid a layout table, role="presentation" on the <table> removes the table semantics. Better is to not need it.

Mistakes we keep finding

Bold first row instead of header cells. The row looks like a header and is marked up as <td> with CSS. Visually correct, structurally absent.

Header cells with no scope in a two-axis table. Products down the side, months across the top. Browsers guess, and they guess differently.

Merged cells with colspan and rowspan. These are legitimate, but as soon as a table needs them the header relationships get complicated enough that headers and id attributes are usually the only reliable way to express them. If a table needs that, ask first whether two simpler tables would serve the reader better.

Nested tables. A table inside a table cell. Almost always a sign that the data wanted a different shape.

No caption on a page with several tables. Not a failure on its own, and a real problem the moment a user pulls up a list of tables and hears “table, table, table”.

A table that cannot reflow. Wide tables are allowed to scroll horizontally, but only inside their own container. See SC 1.4.10 Reflow for how to wrap one without breaking the page.

How to test it

Turn on the tables check in WCAG Radar. It marks every table that has no header cells or no caption, which is the fast way to find both the missing-<th> case and the layout table.

What the check cannot tell you is whether a grid on screen is a table at all. For that, use styles off (reading order). All CSS disappears, and a real table keeps its grid because the browser renders <table> as a grid with no styling at all. A div grid collapses into a list of loose lines. If your opening hours turn into eight stacked fragments, you have your answer.

Then do the reading test. Read the bare table aloud from left to right, row by row, the way a screen reader in table mode would. If a value on its own tells you nothing, its headers are not attached.

Who does what

  • Editors insert tables through the CMS table tool rather than pasting a grid, and mark the header row as a header row. Most editors have that button and most people never press it.
  • Designers avoid designs where a data grid has to be built from cards to get the visual right, and design the narrow-screen version of every table.
  • Developers deliver <th> with scope, a <caption>, and a scroll container for wide tables.

Frequently asked questions

Do I need a caption on every table?

No. A caption matters when a page has more than one table, or when the table sits far from the text that introduces it. Header cells are the part that is not optional.

Is a table without headers automatically a failure?

If it carries data, yes. If it genuinely has no relationships between cells, it is a layout table and should not be a table.

Can I use ARIA table roles instead?

role="table", role="row" and role="columnheader" exist and work, but they are a repair kit for grids you cannot rebuild. Native <table> gives you the same semantics plus keyboard table navigation, for free and without maintenance.

What about a table that is too wide for a phone?

Wrap it in a scroll container with tabindex="0" and role="region" plus a label, so keyboard users can reach the scroll area. That satisfies both this criterion and reflow.

Summary

  • A table is only a table if the code says so. Div grids lose every relationship.
  • <th> with scope is the part that makes values mean something.
  • A <caption> names the table. Add it when a page has more than one.
  • Never use a table for layout, and if you inherit one, role="presentation" removes the semantics.
  • Check with the tables check in WCAG Radar, then turn styles off and see whether the grid survives.

Tables are the component where an automated check gets you furthest, because missing header cells are unambiguous. Whether the table should have been a table at all is the part that needs a person. Ask us for a quote if you want that read properly.

Related Posts

SC 1.3.1 - What does "Info and Relationships" mean?

You can see that a line is a heading because it is big and bold. You can see that three lines belong together because they each start with a bullet. You can see which column a number belongs to because it sits underneath the word “Price”. None of that is visible to a screen reader. It only knows what the code says. Success criterion 1.3.1 is the rule that closes that gap: the structure a sighted visitor perceives must also be available in the markup.

ARIA roles and attributes: when you need them and when you do not

ARIA is the one technology in web accessibility that lets you make a page actively worse than it was without it. A wrong role tells assistive software something untrue, and the user acts on it.