Skip to content

Table

The class goes on a real <table>. It is <th> with scope that lets screen readers say “Amount, £1,240” when the user is in a cell. If you build the table from <div> elements, that connection does not exist, and the numbers have no meaning.

AttributeEffect
data-variant="striped"A muted surface on every other row
data-hoverableHighlights the row under the mouse. Use only when the row can be clicked.
data-align="end" on a cellRight-aligns, for amounts and quantities

Numbers are set in tabular-nums, so the digits have equal width and the column can be read downwards.

A column that can be sorted has a button in its header, and the direction it is sorted in is in aria-sort on the <th>. The arrow is drawn from the same attribute, so what the screen reader says and what the reader sees are the same. The button fills the cell, so the header has the same height and indent as the cells below. Without aria-sort the button shows both arrows, muted.

The sorting itself is up to you. In React the app keeps the column and the direction in state and redraws the rows. If the server sends HTML, in Datastar or a template, it sorts and writes aria-sort on the right header. Without JavaScript the button can be a link, <a class="lt-table__sort" href="?sort=due&direction=ascending">, and the server sorts on the next page load. The link gets the class alone, since lt.tableSort().button also gives type="button".

If you redraw the table after a click, put focus back on the button the user pressed. Otherwise it ends up on <body>. List with filters does this in its demo.

A table wider than the screen must be scrollable sideways, and that area must be reachable with the keyboard:

<div class="lt-table-scroll" tabindex="0" role="region" aria-label="Invoices">
<table class="lt-table">…</table>
</div>

tabindex="0" is not optional. Without it, nobody navigating by keyboard reaches the content beyond the edge. role="region" with a text makes the screen reader say what the area contains when focus lands there.

ClassWhat it is
.lt-tableThe table itself. The variant comes from data-variant.
.lt-table-scrollThe container that lets a wide table scroll
.lt-table__sortThe button in a sortable column header. The arrow is ::after.
VariableDefault
--lt-table-cell-paddingvar(--lt-spacing-2) var(--lt-spacing-3)

See Customising for how the variables and the layer work.

If you need more rows on screen, it is the space in the cells that should go down. That is one variable, and there is no data-size for it:

Leave the font size alone. --lt-font-size-m in the table is the same as the body text, and numbers become harder to read down a column when they shrink. If you still want smaller text, it is an ordinary rule in your own CSS, not a variable, since it is not meant to be changed.

  • Every column header should be <th scope="col">, and the row’s own key <th scope="row">. Then the screen reader knows which column and row a cell belongs to.
  • Give the table a <caption>. It says what the table contains, and is the first thing announced.
  • Never merge cells to make a layout look right. Merged cells make navigation unpredictable.
  • If rows should be clickable, put the link in a cell. A whole row that is clickable cannot be reached by keyboard.
  • aria-sort goes on the <th>, not on the button, and only on the column the table is sorted by. Columns that are not sorted do not have the attribute.
  • Say in the <caption> that the columns can be sorted, for example with .lt-sr-only. The screen reader reads the caption when the user reaches the table, and otherwise the buttons in the header are the only hint.
  • Screen readers announce a change in aria-sort differently, and some not at all. If the change should be spoken, write it in an element with role="status".