Design system Containers
Table
Dense records on desktop queues and lists. A table is a card too — it wears the card dress, and below 768px it becomes one.
.mk-table
Overview
The table is how the workspace shows many records at once: the enquiry queue, the itineraries list. It wears the card dress — hairline border, card radius, low shadow — rather than a heavier outline of its own, because a table is a card that happens to hold rows.
| Name | Value | Status | Owner |
|---|---|---|---|
| First item Supporting detail |
Value | Status | — |
| Second item Supporting detail |
Value | Status | Owner |
| Third item Supporting detail |
Value | Status | Owner |
Usage
- Compare records across named columnsUse a table when readers need to scan the same attributes across multiple rows, sort them, or inspect aligned values.
- Keep cells structurally consistentEvery column owns one data role and every row follows the same reading order. Use List when column comparison is not part of the task.
- Transform rows below 768pxOn small screens, render each row as a card in the same reading order instead of introducing horizontal scrolling.
Anatomy
| Name | Status |
|---|---|
| Item name | Status |
- 1Header band. Caps column heads on the sunken surface, closed by a hairline. It names the facts; it never acts.
- 2Row. One record. Hairline-separated, and the whole row tints on hover when it does something. Identity leads, the operational tail follows.
- 3Cell. 14px on the density variables —
--mk-table-font,--mk-table-pad-y,--mk-table-pad-x— so every dense table in the workspace breathes at the same rate.
Variants and states
The expanding row
The queue's rows open in place. Clicking the row expands it; the caret button holds the row's right corner and mirrors the open state; the detail content sits on white while the open row keeps its wash. The record's one door stays the guest's name — a row that expands is not a row that navigates. Press a row below.
| Name | Value | Status | Expand |
|---|---|---|---|
| First item | Value | Status | |
|
The detail the row was hiding — enough to triage without leaving the list. The panel holds what helps triage the row without leaving the queue; everything deeper lives in the record. |
|||
| Second item | Value | Status | |
|
Another row's detail. |
|||
Sorting
The current table does not sort by its column heads. Column sorting left the table
when the sort became a select on the facet row beside the search, so the heads stay
quiet and name their facts. The layer still carries a specified
th.mk-sortable head — chevron pair, direction by emphasis,
aria-sort on the cell — but no built surface wears it; it waits for a
table whose sorting genuinely belongs in the head.
States
Every state below is the real component wearing that state, not a picture of it — each cell is a compact live table whose second row carries the state.
| State | Specimen | What changes | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| Rest |
|
Hairline-separated records on the card surface; the header band names the facts and never acts. | |||||||||
| Row hover |
|
The whole row takes the faint wash when the row does something. A table whose rows do nothing does not hover. | |||||||||
| Open |
|
The expanded row keeps a quiet sunken wash and hands its bottom border to the detail row, so the two read as one object. The caret mirrors the state. | |||||||||
| Keyboard focus |
|
The row is reachable; focus is the standard 2px camel outline drawn
inward. The caret button carries aria-expanded and answers for
the keyboard. |
|||||||||
Do and don't
Below 768px the table becomes this card, and the card keeps the row's reading order — identity, then dates and route, then the operational tail.
| Guest / party | Dates | Status | Owner | Source |
|---|---|---|---|---|
| Okafor ×2 | 21–28 Feb | Deposit pending | A. Said |
Horizontal scroll hides columns behind an edge the agent cannot see. A dense table never scrolls sideways — it becomes cards instead.
A guest name wraps; it is never truncated — a half-shown name is a different
guest. A reference truncates; it is never wrapped, with the full value in
title.
Identity cut mid-word. Names survive the layout; the layout does not survive the review.
Copy
A column head names the fact the column holds, in the agent's words.
- The head names the factGuest / party, Dates, Owner — one or two words. A head that needs a sentence is a column that holds two facts.
- No units in cellsThe head carries what is constant — Nights — and the cell carries only the value. A column of "7 nights" says "nights" forty times.
- Absence is a dashAn unassigned owner is —, not "n/a", not "none", not an empty cell that reads as a loading error.
- Dates without the year14–21 Nov. The workspace's horizon is months, and the year returns only when it is genuinely ambiguous.
- Facts are not gluedA subline is a sentence with commas — 2 adults, one child — or separate elements with air. The «·» string is banned as fact glue.
Responsive
Below 768px a dense table becomes a card list. It never scrolls sideways. The card keeps the same reading order as the row — identity, then dates and route, then the operational tail — so nothing has to be re-learned between a desk and a phone.
- The boundary768px, the table-to-cards line for every dense table in the workspace.
- Density holdsThe
--mk-table-*variables do not shrink on a phone — a table that gets tighter as the screen narrows is optimising the wrong thing. - The whole card is the targetWhen the row navigated, the card navigates — one stretched link, no duplicate "Open" button.
Accessibility
- Scope on every head
<th scope="col">, so a screen reader announces the column with each cell. - The expander is a real buttonThe caret carries
aria-expandedand an accessible name for both directions; the detail row ishiddenwhen closed, not merely collapsed. - The name is the doorOpening the record is the name link's job, so a keyboard user is never forced through an expanded row to reach it.
- Hover is not the signalThe row tint invites, but everything a hover reveals is reachable and readable without one.
- Motion respects the settingThe caret's rotation stops under
prefers-reduced-motion; the state is still spoken byaria-expandedand the row's wash.
When to reach for something else
- One record as a tileUse Card — which is also what every table becomes below 768px.
- A handful of facts about one objectUse facts and tags in the record itself. A table of one row is a form wearing the wrong clothes.
- Content that panelsUse Widget. A table is for records that repeat, not for sections that differ.
Markup
<table class="mk-table">
<thead>
<tr>
<th scope="col">Guest / party</th>
<th scope="col">Dates</th>
<th scope="col">Status</th>
<th scope="col"><span class="mk-sr-only">Expand</span></th>
</tr>
</thead>
<tbody>
<!-- the expanding pair: the row, then its detail row -->
<tr class="mk-row-expand">
<td><span class="mk-guest">Okafor ×2</span>
<div class="mk-subline">Anniversary, surprise flag</div></td>
<td>21–28 Feb</td>
<td><span class="mk-badge sz-s">…</span></td>
<td>
<button class="mk-icon-btn" type="button" aria-expanded="false"
aria-label="Expand row details">
<svg data-mk-icon="chevron" aria-hidden="true"></svg>
</button>
</td>
</tr>
<tr class="mk-row-detail" hidden>
<td colspan="4"><div class="mk-row-detail-panel">…</div></td>
</tr>
</tbody>
</table>
<!-- its mobile form: same record, same reading order -->
<a class="mk-card" href="…">
<div class="row1">
<span class="name">Okafor ×2</span>
<span class="mk-badge sz-s">…</span>
</div>
<div class="row2">21–28 Feb, anniversary, surprise flag</div>
</a>