Skip to content
milan.swiss Halbton v0.1.0
Concept project

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.
Open invoices, newest first. Amounts in CHF.
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

the structure that matters
<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

None of these are optional, and all of them are cheap.
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.