Components
Data table
Rows of comparable records. The design work here is almost entirely about making columns scannable down rather than making the table look impressive.
When to use it
When records share fields and the user needs to compare them across rows. If they will only ever look at one record, that is a detail view, and a table is a worse way to read it.
When not to
- Never for layout. A table communicates row and column relationships to a screen reader. Using one for arrangement announces relationships that do not exist.
- Not for a handful of key-value pairs. That is a description list.
- Not as a wide table on a phone. Below about 40rem, collapse each row into a stacked card. Horizontal scrolling is a fallback, not a design.
| Invoice | Client | Status | Amount | Due |
|---|---|---|---|---|
| 2026-0114 | Aare Labs | Paid | 4,200.00 | 13 Feb |
| 2026-0113 | Studio Nord | Sent | 1,850.00 | 28 Jan |
| 2026-0112 | Vogt & Co | Overdue | 12,400.00 | 04 Jan |
| 2026-0111 | Hafen AG | Paid | 760.00 | 22 Dec |
Numbers
Right-aligned, monospaced and set in tabular numerals, so digits line up in columns and 12,400.00 is visibly an order of magnitude above 1,850.00 without reading either. Proportional figures make that comparison require actual reading, on every row.
Markup
<div class="ht-tablewrap" role="region"
aria-label="Invoices" tabindex="0">
<table class="ht-dt">
<caption>Open invoices, newest first.</caption>
<thead>
<tr>
<th scope="col">Invoice</th>
<th scope="col">Client</th>
<th scope="col" class="ht-dt__num">Amount</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">2026-0114</th>
<td>Aare Labs</td>
<td class="ht-dt__num">4,200.00</td>
</tr>
</tbody>
</table>
</div> Structure
| Element | Why |
|---|---|
| <caption> | Names the table. Without it a screen reader announces "table, five columns" and leaves the user to infer the rest. |
| th scope="col" | Binds each cell to its column header, so a cell is read as "Amount, 4200" rather than as a bare number. |
| th scope="row" | Marks the identifying column, so a row can be read back as "invoice 2026-0114, paid". |
| role="region" tabindex="0" | On the scroll container. A region that scrolls but cannot be focused hides content from keyboard users entirely. |
Status is never colour alone
Every status cell above carries a word. The tint is reinforcement — for the roughly one in twelve men with a red-green deficiency, and for anyone on a poor screen in bright light, the word is the whole message.
Sorting
When a column is sortable, the header is a real <button> inside the <th>, and the <th> carries aria-sort of ascending, descending or none. An arrow glyph with no aria-sort tells a screen-reader user nothing about how the data they are reading is ordered.