Skip to content

Table

Tier B. Follows the anatomy template and the conventions.

Shows data in rows and columns, to compare and look up values. Renders a native <table> with a <caption>, <thead> and <tbody>, and header cells with scope="col": the browser gives the table semantics, so a screen reader announces each cell with its column header. A sortable column’s header holds a native <button>, and the sorted column carries aria-sort, following the WAI-ARIA sortable table example. A table that is edited in place or navigated cell by cell is a grid (Tier C), not a Table.

A raised slab, like a Card, holding the table. Rows are separated by hairlines and the header row by a stronger line; none of them has depth.

Part Shape Height at 50% Moves
Slab Rounded plate, radius.control, bevel.width band elevation.raised (3) No
Header row The top row, with a 2 px edge line under it Flat (in the captured content) No
Row At least the density’s control height, a 1 px line between rows Flat No
Sort button The header cell’s content, at least target.min (24) high Flat No
Face Colour role Text role Material role
Slab secondary text control (matte)
Caption None text, strong None
Header cell None text, strong, font.size.small None
Row, hover A 4% text tint — None
Row separator edge at 50% — None
Sort indicator — text None

Numeric columns are aligned to the end, with tabular figures. data-strata-material on the table overrides the slab’s material.

State Relief Objects CSS
Rest Slab at elevation.raised Slab at raised height Relief recipe at raised
Row hover A 4% tint Same Same
Sort button hover, pressed A 6% or 10% tint on the header cell Same Same
Sort button focus-visible outline: focus.width solid accent, inset Same Same
Sorted (aria-sort) An arrow after the header’s label, up for ascending, down for descending Same Same

The slab does not rise or travel. Selected, invalid, disabled and busy rows do not apply; a busy table shows a ProgressBar or Spinner near it.

One raised primitive: the .ug-table wrapper, from its border box. The table is captured content on it.

Token Prop
colour.secondary, colour.text, colour.edge —
radius.control — (theme or scope)
elevation.raised, bevel.*, shadow.* —
material.roles.control material
control.height, target.min, space.*, density, font.size.body, font.size.small size
focus.width —
level level
<div class="ug-table" data-strata-face="raised">
<table class="ug-table__table">
<caption class="ug-table__caption">Invoices</caption>
<thead>
<tr>
<th class="ug-table__header" scope="col" aria-sort="ascending">
<button class="ug-table__sort" type="button">Number</button>
</th>
<th class="ug-table__header" scope="col">
<button class="ug-table__sort" type="button">Customer</button>
</th>
<th class="ug-table__header ug-table__header--numeric" scope="col">Amount</th>
</tr>
</thead>
<tbody>
<tr class="ug-table__row">
<td class="ug-table__cell">1001</td>
<td class="ug-table__cell">Ada Lovelace</td>
<td class="ug-table__cell ug-table__cell--numeric">120.00</td>
</tr>
<tr class="ug-table__row">
<td class="ug-table__cell">1002</td>
<td class="ug-table__cell">Grace Hopper</td>
<td class="ug-table__cell ug-table__cell--numeric">87.50</td>
</tr>
</tbody>
</table>
</div>

A table that does not sort (and Astro’s, which renders no sort buttons) has plain header cells; aria-sort still says the order the rows are in:

<div class="ug-table" data-strata-face="raised">
<table class="ug-table__table">
<caption class="ug-table__caption">Invoices</caption>
<thead>
<tr>
<th class="ug-table__header" scope="col" aria-sort="ascending">Number</th>
<th class="ug-table__header" scope="col">Customer</th>
<th class="ug-table__header ug-table__header--numeric" scope="col">Amount</th>
</tr>
</thead>
<tbody>
<tr class="ug-table__row">
<td class="ug-table__cell">1001</td>
<td class="ug-table__cell">Ada Lovelace</td>
<td class="ug-table__cell ug-table__cell--numeric">120.00</td>
</tr>
</tbody>
</table>
</div>
  • The wrapper <div> is the boundary element and the slab: refs, class and style reach it. It does not scroll; put a wide table in a scroll container of your own (a focusable region with a name).
  • Sorting: a sortable column’s header holds a .ug-table__sort button. Only the sorted column has aria-sort (ascending or descending); a click on its button reverses the order, and a click on another column’s button sorts by it, ascending. The button’s name is the column’s name; aria-sort says the order.
  • React and Vue: <Table caption columns rows> with columns: { key, header, numeric?, sortable? }[] and rows as records of cell values (rowKey names the column that identifies a row, the first by default). Sorting is controlled by sort ({ key, direction }) and onSortChange (v-model:sort in Vue), or left to the component with defaultSort, which sorts numbers by value and text with localeCompare. Astro renders the table with the headers’ aria-sort and no sort buttons: sorting needs script.
  • No overlay.
Key Action
Tab, Shift+Tab Move between the sort buttons and any controls in cells (native order)
Enter, Space Sort by the focused column, or reverse its order (native button)
  • The table is named by its caption. Each cell is announced with its column header; the sorted column says “sorted ascending” or “descending”.
  • Screen reader table navigation (Ctrl+Alt+arrows and the like) works as on any native table.

The raised recipe on the wrapper, with the card’s secondary colour and space.2 padding. The table is border-collapse: collapse and full width. Cells have space.3 and space.4 padding and a 1 px separator under each row except the last; the header row has a 2 px edge line. The sort arrow is a small triangle drawn by borders. Under forced-colors the slab has a CanvasText border and the separators are CanvasText.

  • Bindings: React, Vue and Astro render exactly the markup above; a click, Enter and Space on a sort button sort the rows and move aria-sort; controlled sort follows from outside; numeric columns sort by value; roles, names and header association; axe in light and dark.
  • GPU: at Relief and Objects the table is one raised slab (an object slab at Objects) and its rows add nothing to the scene; a real click on a sort button through the GPU sorts the rows, with GPU on.