A table is for data
Tables are for information with two dimensions — rows and columns that both mean something. They are not a layout tool; that has been CSS grid's job for years.
A table anyone can read needs three things beyond the data: a <caption>, real <th> header cells, and scope saying whether each header heads a column or a row.
<table>
<caption>Lead times by finish</caption>
<thead>
<tr>
<th scope="col">Finish</th>
<th scope="col">Stock</th>
<th scope="col">Made to order</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Painted</th>
<td>5 days</td>
<td>3 weeks</td>
</tr>
<tr>
<th scope="row">Veneer</th>
<td>10 days</td>
<td>5 weeks</td>
</tr>
</tbody>
</table>
The parts
| Element | What it is |
|---|---|
<caption> | The table's name and its accessible name. Must be the first child. |
<thead> <tbody> <tfoot> | Row groups. tfoot is for totals and may be written last. |
<tr> | A row. |
<th> | A header cell. Takes scope="col", "row", "colgroup" or "rowgroup". |
<td> | A data cell. |
colspan / rowspan | How many columns or rows the cell covers. |
<colgroup> / <col> | Lets you size or style whole columns at once. |
Why scope matters
With scope, a screen reader can announce a cell together with its headers — "Veneer, made to order, 5 weeks" — instead of a bare "5 weeks" with no idea which row or column it belongs to. It costs one attribute per header.
If headers span in both directions, scope is not enough: give each <th> an id and each <td> a headers="id1 id2". If you need that, first ask whether two simpler tables would be better.
Spans
Every row must still add up to the same number of columns once spans are counted. A miscount silently shifts the rest of the table.
<tr>
<th scope="row">Total</th>
<td colspan="2">15 days</td>
</tr>
Sizing columns
table-layout: fixed in CSS makes the browser use the first row and your column widths instead of measuring all the content — much faster on a long table, and it stops one long word widening a column.
<table>
<colgroup>
<col style="width: 40%">
<col span="2">
</colgroup>
…
</table>
Tables on a phone
A wide table will not fit a small screen, and shrinking the text is not a fix. Put the table in a container that scrolls sideways on its own, inside a page that does not. Give the container a tabindex so it can be scrolled by keyboard, and a label so it is announced.
<div class="tablewrap" tabindex="0" role="region" aria-label="Lead times">
<table>…</table>
</div>
Numbers
Right-align numbers and use tabular figures so the digits sit in columns. It makes a table of prices readable at a glance.
td.num {
text-align: right;
font-variant-numeric: tabular-nums;
}