Noticea11y.table-headers

Data tables without header cells

Why it matters

Looking at a table, you read a cell by glancing up to its column heading and across to its row. A screen reader does the same aloud ("Price: £320") but only if the headings are marked up as <th>. When every cell is a <td>, even the bold ones in the first row, what is heard is a run of values: "Six inch, twelve, three hundred and twenty pounds, eight inch…".

A notice, because we are guessing which tables hold data. A table with a single row or column, one that contains another table, or one marked role="presentation" is taken to be layout and left alone; the rest are assumed to be data. If one we flag is really there for layout, say so with role="presentation", or better, lay it out with CSS.

Source: WCAG 2.2, 1.3.1 Info and Relationships (Level A), by technique H51; W3C: tables tutorial.

How to fix it

Use <th> for the headings, with scope to say which way they apply:

<table>
    <tr><th scope="col">Size</th><th scope="col">Serves</th><th scope="col">Price</th></tr>
    <tr><th scope="row">6 inch</th><td>12</td><td>£320</td></tr>
</table>

Examples

These are the rule's own test pages, and what it says about each. They are run on every change to the rule book.

<table class="prices">
<td>
<b>Size</b>
</td>
<td>
<b>Serves</b>
</td>
<td>
<b>Price</b>
</td>
<td>6 inch</td>
<td>12</td>
<td>£320</td>

Notice1 table has no header cells (the first has 2 rows and 3 columns), so a screen reader reads the values with nothing to say what they are: table.prices.

fail-all-td
Example page
<table class="prices">
<td>
<b>Size</b>
</td>
<td>
<b>Serves</b>
</td>
<td>
<b>Price</b>
</td>
<td>6 inch</td>
<td>12</td>
<td>£320</td>
<table role="presentation">
<td>
<img src="/logo.png" alt="Flour &amp; Co">
</td>
<td>12 Call Lane</td>
<td>
</td>
<td>Leeds</td>

PassedEvery data table has header cells (0 tables).

pass-layout
<table class="prices">
<th scope="col">Size</th>
<th scope="col">Serves</th>
<th scope="col">Price</th>
<td>6 inch</td>
<td>12</td>
<td>£320</td>

PassedEvery data table has header cells (1 table).

pass-th