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.

NameRoleOfficeDays
Ada LovelaceEngineerMarylebone Road3
Mary SomervilleWriterChelsea12
Charles BabbageDesignerDorset Street7
Michael FaradayResearchAlbemarle Street120

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.

Days on site this quarter
NameDaysShare
Ada Lovelace32.1%
Mary Somerville128.5%
Charles Babbage74.9%
Michael Faraday12084.5%
Total142100.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.

PartCarries
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.

NameRoleActions
Ada LovelaceEngineer
Mary SomervilleWriter
Charles BabbageDesigner
Michael FaradayResearch

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

NameRole
Ada LovelaceEngineer
Mary SomervilleWriter
Charles BabbageDesigner
Michael FaradayResearch

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”.

NameRoleOfficeEmailManagerStartedQ1Q2Q3Q4Total
Ada LovelaceEngineerMarylebone Roadada@example.orgMary Somerville10 Dec 2015331613
Mary SomervilleWriterChelseamary@example.orgMichael Faraday26 Dec 20101217102463
Charles BabbageDesignerDorset Streetcharles@example.orgMary Somerville26 Dec 201171751443
Michael FaradayResearchAlbemarle Streetmichael@example.orgAda Lovelace22 Sep 2008120135118240613

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.