8. Tables
For data that benefits from comparing across rows. The primitive most easily abused — often reached for when a list of cards would scan better.
Gallery
Canonical table at standard density. Hover any row to see the tint; the third row is selected via aria-selected="true".
| Name | Status | Owner | Updated | |
|---|---|---|---|---|
| Plate-A03 | Ready | imad | 2 min ago | |
| Plate-A04 | Running | df | 4 min ago | |
| Plate-A05 | Stalled | imad | just now |
- Header · uppercase, tracking-wider, semibold, muted color (set on
.table th) - Row hover · subtle tint via
.table-interactive - Selected row ·
aria-selected="true", primary-subtle background - Row action ·
.btn-subtle.btn-icon-onlyat row end — doesn't compete - Status column · composes with badge primitive (Section 4)
When to use each
Three decisions that compound: should this even be a table, how dense, and how do rows interact?
| Name | Status | Updated |
|---|---|---|
| Plate-A03 | Ready | 2m |
| Plate-A04 | Running | 4m |
| Plate-A05 | Stalled | now |
Two-step protocol for amplifying target DNA across 96-well plates.
De-lid source plates and seal destination plates in a single workflow.
| Run | Plates | Status |
|---|---|---|
| PCR-024 | 96 | Ready |
| LID-009 | 24 | Running |
| STG-201 | 48 | Stalled |
| PCR-023 | 96 | Ready |
.table-compact)| Run | Plates | Status |
|---|---|---|
| PCR-024 | 96 | Ready |
| LID-009 | 24 | Running |
.table-comfortable)| Name | Status |
|---|---|
| Plate-A03 | Ready |
| Plate-A04 | Running |
.table-interactive. | Name | Status | |
|---|---|---|
| Plate-A03 | Ready | |
| Plate-A04 | Running |
.table-interactive — the row isn't clickable. Drift to avoid
Seven named anti-patterns. Tables are where most products lose their visual discipline — every one of these is common.
- Don't put Title Case in column headersPrinciple 2Convention is uppercase, semibold,
--tracking-wider, muted color — baked into.table th. Use Title Case only in card headers or section titles, where the heading is the focus. In tables, the header is scaffolding, not content. - Don't zebra-stripe rowsPrinciple 4Striped backgrounds feel old, fight hover/selected state, and add visual noise without adding meaning. Horizontal dividers between rows do the same scanning job without the cost.
- Don't draw vertical borders between cellsPrinciple 1Vertical lines turn a table into a spreadsheet. Reserve them for spreadsheets. UI tables use horizontal dividers only — the column alignment plus typography does the work.
- Don't right-align text columnsUXText reads left-to-right; ragged left edges are hard to scan. Right-align only numeric columns and short tokens (durations, counts, percentages) with
.table-num, which also appliesfont-variant-numeric: tabular-numsso digits align cleanly. - Don't make hover state saturatedPrinciple 4A strongly-colored hover competes with the selected-row treatment — both look like "the chosen one." Hover is a subtle tint (
--color-bg-hover); selection is the saturated state (--color-primary-subtle). Keep the contrast. - Don't ship a table without an empty stateUXAn empty table is a missed opportunity. Replace the rows with a deliberate empty state: icon, headline, one-sentence explanation, primary action ("Add first…"). See Section 12.
- Don't cram every action into its own columnPrinciple 1"Edit" / "Duplicate" / "Delete" columns triple the width and bury the data. Collapse all per-row actions behind a single
.btn-subtle.btn-icon-onlyoverflow menu at the row end. The data is what the user came for.
Composition — method run history
A richer table using everything above: sortable header, mixed text and numeric columns, status badges from Section 4, one selected row, overflow action menus, pagination. Each column alignment is intentional.
| Method | Run | Started | Duration | Status | Plates | ||
|---|---|---|---|---|---|---|---|
| PCR amplification | PCR-024 | 2026-05-12 14:32 | 4h 12m | Ready | 96 | ||
| Lid transfer | LID-009 | 2026-05-12 13:15 | 0h 47m | Running | 24 | ||
| Storage retrieval | STG-201 | 2026-05-12 09:04 | 1h 23m | Warning | 48 | ||
| PCR amplification | PCR-023 | 2026-05-11 18:22 | 3h 58m | Ready | 96 | ||
| Decapping | DCP-117 | 2026-05-11 11:00 | 0h 12m | Error | 12 |