· 5 min read
How to Write Accessible HTML Table Markup
Manesh Jayawardhana
CIO & Co-founder
You need a six-row pricing table in a page, and you know roughly what the markup looks like — but not well enough to type it from memory without checking whether the header row goes inside thead, and whether scope is one of those attributes that stopped mattering years ago.
So you copy a table from somewhere else in the codebase, delete the content, and inherit whatever mistakes it already had. That’s how a project ends up with eleven tables and four different structures, two of which are unreadable with a screen reader.
What table markup actually needs
An HTML table has more structural parts than most people use. The ones that matter are thead for header rows, tbody for the data, th for header cells, and td for data cells.
The critical piece is scope. A th with scope="col" tells assistive technology that this header describes the column beneath it; scope="row" says it describes the row beside it. Without that, a screen reader reaching a cell in the middle of the table can only read the raw value — “£49” — with no way to say which column and row it belongs to.
Sighted readers get that association free, by looking. Everyone else gets it from the markup or not at all. The W3C’s tables tutorial walks through the cases in detail, including the awkward ones with two levels of header.
Why people get stuck here
- Copying whatever’s nearby. Existing project tables are the most common source of new project tables, mistakes included.
- Confusing visual and semantic headers. Bolding the top row with CSS makes it look like a header. It doesn’t make it one.
- Tables for layout. Long dead on the web, still standard in HTML email, which is why the habit keeps resurfacing.
- Two-dimensional data with one header. Tables with both a top row and a first column of labels need
scopeon both, and that’s the case people most often get wrong.
What good table markup looks like
Header cells are th, with a scope
Every header cell should be th rather than a styled td, and every one should declare its scope. It’s two extra attributes per table and it’s the difference between a data table and a grid of unlabelled numbers.
Sections are explicit
thead and tbody let a browser and a screen reader treat headers as headers — including when a long table’s header row is repeated across printed pages. Skip them and you lose both behaviours.
Structure first, styling second
Generated markup should be plain and inherit your stylesheet, not arrive with inline colours and widths baked in. Structural markup survives a redesign; hard-coded presentation doesn’t.
| Element | Purpose | Skip It And |
|---|---|---|
thead / tbody | Separates headers from data | Header rows won’t repeat when printed |
th scope="col" | Names the column | Screen readers read bare values |
th scope="row" | Names the row | Two-way tables become unnavigable |
Common mistakes to avoid
- Using
tdwith bold styling where athbelongs — visually identical, semantically empty. - Adding
scope="col"to the first column’s header cells when they should bescope="row". - Nesting a table inside a table for layout reasons, on a web page rather than in an email.
- Leaving the
captionelement out on a table that needs a title, then adding a paragraph above it instead. - Using a table for content that’s really a list — a table with one data column is a list wearing the wrong element.
How to do it with HTML Table Generator
The HTML Table Generator produces the structure with the boring parts already correct, so you only replace the content.
- Set the number of rows and columns you need.
- Choose whether the header runs across the top, down the first column, or both.
- Generate the markup — it comes with
thead,tbodyand the rightscopeon every header cell. - Paste it into your page and swap the placeholder text for your data.
- Check it with a keyboard and, if you can, a screen reader before shipping.
If you’re going the other direction and need to get tabular data out of a page, the tools directory has CSV and HTML converters that run in the browser too.
Frequently asked questions
Do I still need scope attributes in modern browsers?
Yes. Browsers don’t need them for rendering, but assistive technology does for navigation. Some screen readers can infer simple cases, and none can reliably infer a table with headers on two axes — which is exactly when a reader most needs the association.
Is it ever acceptable to use tables for layout?
On the web, no — CSS grid and flexbox exist for that. In HTML email it’s still the practical choice, because email clients support a fraction of modern CSS. Add role="presentation" to a layout table so assistive technology skips its structure.
Should the generated table include CSS?
Better if it doesn’t. Structural markup that inherits your existing stylesheet drops into any project cleanly. Markup with inline widths and colours fights the design system on arrival.
Final thought
The test for whether a table is marked up correctly isn’t how it looks — it’s whether someone navigating cell by cell can tell what any single value means. If a cell’s meaning depends on being able to see the top of the column, the markup isn’t finished.