Table
Rows of records, in a real table.
import { Table, TableHead, TableBody, TableRow, TableCell } from "@dede-style/ui/table", then compose it the way you would write the HTML. The element already knows how to be a table; these parts add the system’s chrome and the few decisions a table has to carry, and nothing else.
| Name | Role | Office | Days |
|---|---|---|---|
| Ada Lovelace | Engineer | Marylebone Road | 3 |
| Mary Somerville | Writer | Chelsea | 12 |
| Charles Babbage | Designer | Dorset Street | 7 |
| Michael Faraday | Research | Albemarle Street | 120 |
Six parts, and the browser is the seventh. Table is the <table>; TableHead, TableBody and TableFoot are its sections; TableRow is a row; TableCell is a cell, and becomes a header cell the moment you give it a scope. Every part renders the element it is named for, so the roles, the keyboard and the screen-reader grid arrive with the markup rather than being re-implemented.
What each part carries is only the decision that belongs to it. The table decides its size, its block-axis alignment, its hairlines and its washes, because a table whose rows disagree about any of those is broken. A cell decides which way it justifies, whether it is a number, and whether it drops its inset. A row decides one thing: whether it is shown as selected.
Cells
Text starts; numbers end; a header follows its column.
| Name | Days | Share |
|---|---|---|
| Ada Lovelace | 3 | 2.1% |
| Mary Somerville | 12 | 8.5% |
| Charles Babbage | 7 | 4.9% |
| Michael Faraday | 120 | 84.5% |
| Total | 142 | 100.0% |
justify is the inline axis and align the block axis, on a table as on every primitive here. So a cell’s horizontal position is justify="end", per cell, because a row genuinely mixes text and numbers; and the vertical position is align on the table, because a row whose cells disagree about it reads as a mistake.
numeric does two things a number needs: it justifies end unless told otherwise, and it sets tabular figures, so every digit takes the same width and a column of them lines up on its units. The header above a numeric column carries the same justify as the column — a heading over the wrong edge of its numbers is the commonest table mistake there is. A first-column scope="row" names the row for a screen reader and takes the weight a name earns; the caption names the whole table.
Alignment
Baseline keeps a chip and a wrapped sentence on one line; middle would not.
| Part | Carries |
|---|---|
Table <table> | Size, block-axis alignment, hover, stripes, dividers and the caption — everything true of the whole table. |
TableCell <td> | Which way it justifies, whether it is a number, whether it takes a named padding case; a scope makes it a <th>. |
TableContainer <div> | Inline overflow, as a focusable, named region — the one part a consumer adds only when the answer is scroll. |
align only shows itself when a row wraps or mixes type sizes, and this one does both. Each part name carries a code chip at nine-tenths of the body size; the description runs to two lines. Under baseline, the default, the name, the chip and the first line of the description all sit on one line, because baseline aligns the text itself and not the boxes around it. middle would centre each cell on the row, so the name and the chip would drop half a line as the description grew — right for a row of single-line things, wrong for one you read.
top is for the row this example is not: cells that are deliberately different sizes, a 60px type specimen beside a 14px token name, where baseline would drop the name to the specimen’s baseline. For a chip or a caption beside body text it is the wrong tool — it aligns box tops, so the smaller text sits visibly higher than the words beside it. A control cell is the other exception, and it aligns itself; see below.
Size, and a control in a row
The row height is the control ramp, and a control sits one step below its row.
| Name | Role | Actions |
|---|---|---|
| Ada Lovelace | Engineer | |
| Mary Somerville | Writer | |
| Charles Babbage | Designer | |
| Michael Faraday | Research |
size is a --size-element-* step: 32, 40 or 48 for a single-line row. The same ramp Button and TextInput take, so the two agree by construction. The inset is derived from the row and the line height, so a row that wraps grows by whole lines and a row that does not is exactly its step; the inline inset ramps with it, 8, 12 and 16, because density is the reason the small size exists.
A control sits one step below its row — these are 32px buttons in 40px rows, 4px above and below, with each glyph on the column line under its header. That is padding="control" on the actions cell: a named case rather than a number, because a cell takes the table’s inset or has a reason not to, and a per-cell number would be a way to make a row disagree with itself. The cell also pins itself to the top of the row, because an icon-only control has no text and therefore no baseline — left to the table’s align, the browser would take its bottom edge and drag the whole row taller. Pinned, it sits centred on the first line and stays there if a neighbour wraps. hover is on because these rows are things you act on.
Row selection
The checkbox is the control; the row only shows the answer.
None selected
| Name | Role | |
|---|---|---|
| Ada Lovelace | Engineer | |
| Mary Somerville | Writer | |
| Charles Babbage | Designer | |
| Michael Faraday | Research |
The consumer holds the set. Nothing in the table knows which rows are chosen. Whoever renders it keeps the set, gives each row a small Checkbox that reports a change, and passes selected to the rows in it. The header box is derived from the same set — checked, mixed or plain — so it is never a second source of truth. selected is a wash and nothing else: one step above a stripe, which it replaces rather than stacks on, with hover composing over it as it does over a stripe. The name keeps its weight, because a weight change would move the text.
The checkbox is the accessible state, so there is no aria-selected. That attribute is valid on a row only inside a grid, and a grid promises a keyboard contract — arrow keys moving cell to cell — that nothing here implements. Each box is named for its row, so a screen reader hears “Select Ada Lovelace, checked” and nothing twice. And a row does not select on click: a row holds links, buttons and selects, so a click on it is ambiguous. The cell is padding="checkbox": a 16px box centred in the row by arithmetic, on the column line, in a column no wider than the box and its insets.
Overflow
Wrap or scroll is the consumer’s call, and this is the part for “scroll”.
| Name | Role | Office | Manager | Started | Q1 | Q2 | Q3 | Q4 | Total | |
|---|---|---|---|---|---|---|---|---|---|---|
| Ada Lovelace | Engineer | Marylebone Road | ada@example.org | Mary Somerville | 10 Dec 2015 | 3 | 3 | 1 | 6 | 13 |
| Mary Somerville | Writer | Chelsea | mary@example.org | Michael Faraday | 26 Dec 2010 | 12 | 17 | 10 | 24 | 63 |
| Charles Babbage | Designer | Dorset Street | charles@example.org | Mary Somerville | 26 Dec 2011 | 7 | 17 | 5 | 14 | 43 |
| Michael Faraday | Research | Albemarle Street | michael@example.org | Ada Lovelace | 22 Sep 2008 | 120 | 135 | 118 | 240 | 613 |
The table itself never scrolls and never clips. The two tables this site already renders disagree about overflow and each is right for its content: the token tables wrap, because a scroll would hide the preview column on a phone; the props table scrolls, because trimming its columns broke prop names in half. So overflow is not a decision the table can make, and TableContainer is what a consumer reaches for when the answer is scroll.
It is focusable and named, because a keyboard has no other way to scroll it. Tab to it and use the arrow keys. The stripe is here for the same reason it exists at all: eleven columns is where the eye starts losing the row.
What is reserved
Sorting and expansion are not built, and each has its trigger written down. A sortable header arrives with the first consumer that sorts, as a button inside the header cell with a glyph for the direction. Expansion arrives with the first row that discloses a detail row beneath it, as expanded. Neither is built ahead of its consumer, on the rule that a prop nothing uses is a promise nothing tests. Selection was the third of these and is built above: its trigger was a Checkbox, and the Checkbox arrived.
A data-driven mode — columns and data instead of children — is the same answer. It can be built on these parts later; the parts could never be recovered from it.