Test your website →

Blog

Accessible Data Tables Without the Headache

Most inaccessible tables aren't hard problems; they're tables that were never marked up as tables in the first place.

Accessible data tables have a reputation as one of the fiddly parts of web accessibility, the place where you finally need to learn a thicket of ARIA attributes and hire a specialist. I want to argue the opposite. Tables are one of the places where accessibility is nearly free, because HTML solved the problem decades ago. The headache isn’t in the solution. The headache is that teams keep declining to use it.

Here’s what a sighted person gets from a table without thinking about it. You look at a cell, your eye flicks up to the column header, flicks left to the row header, and you know what the number means. Four hundred twelve, in the March column, in the Refunds row. That glance is the entire value of a table: the position of a cell tells you what it is.

Now consider what a blind user’s screen reader has to work with. It can’t glance. It reads the page as a structure, and it can only convey relationships that exist in the markup. If your table is built from real table elements, with header cells marked as headers, the screen reader does something quietly wonderful: as the user moves into any cell, it announces the relevant headers along with the value. “Refunds, March, 412.” The user gets the same information the glance provided, in a different medium. This has worked since before most current web developers wrote their first line of code. The th element, the scope attribute that says whether a header applies to its column or its row, a caption element naming the table. That’s the whole toolkit for the vast majority of tables. No framework, no ARIA gymnastics, no specialist.

So why do audits keep finding broken tables? When GazeSite scans a page, the failures cluster into two patterns, and both are self-inflicted.

The first is the fake table: tabular data laid out with divs and CSS grid, or styled spans, because the developer was in a div-writing rhythm or the design system made divs easier. It looks identical on screen. In the markup there is no table, so there are no rows, no headers, no relationships, and the screen reader reads a flat stream of numbers with nothing attaching them to anything. “412. 388. 415. 402.” It’s like reading a spreadsheet to someone over the phone by reciting the cells in order and never mentioning the headers. Every visual relationship the sighted user gets for free has been withheld from everyone else, and the maddening part is that the honest version, an actual table element, would have been less code.

The second pattern is the table that is a table but never says which cells are headers. Everything is a td; the top row just happens to be bold. Visually, bold text reads as a header. Structurally, it’s a cell like any other, so the screen reader has no idea it should announce “March” when the user lands on a number in March’s column. This one stings because the fix is a find-and-replace: promote the header cells to th, add scope, done. Minutes of work, and the difference between a table that can be read and one that can only be recited.

There’s an inverse crime worth naming too: using tables for things that aren’t tabular, or dumping data into a table that should have been a sentence. If you have three key-value pairs, that’s three lines of text, not a table. Tables earn their structure when there are two dimensions of meaning, when a cell’s value depends on both its row and its column. Respecting that boundary helps everyone, because a table is also a cognitive demand on sighted users; every table asks its reader to hold headers in mind while scanning.

The one place tables genuinely get hard is small screens, and it’s worth being honest about it. A ten-column table can’t fit on a phone, and every workaround trades something away. Horizontal scrolling preserves the structure but hides most of it off-screen. Reflowing each row into a stacked card reads nicely but destroys column comparison, which may have been the point of the table. Choosing which columns to hide is an editorial judgment about which data matters least. There’s no universal answer, but notice that this is a design problem, not an accessibility problem, and solving it well tends to help accessibility as a side effect, because the same question drives both: what is this table actually for? A table whose purpose you can state is a table you can adapt. A table that exists because the data came out of the database in that shape will fail on mobile and fail with screen readers for the same underlying reason, which is that nobody decided what it was supposed to communicate.

The business case here is unusually direct, because of where tables live. They’re not decoration; they show up at the load-bearing points of a site, in pricing pages, in comparison charts, in dashboards, in order histories, in spec sheets. These are the pages where a visitor is deciding whether to buy or checking whether they got what they paid for. A pricing table that a screen reader recites as a stream of untethered numbers is a pricing page that cannot sell to that visitor at all.

Use the table elements. Mark the headers. Add a caption. Say what the table is for. It’s less work than the workarounds, and it’s the rare corner of accessibility where the right thing and the lazy thing point in the same direction.

More articles

← All posts