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.

NameValueStatusOwner
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

1
NameStatus
Item name Status
2 3
  1. 1Header band. Caps column heads on the sunken surface, closed by a hairline. It names the facts; it never acts.
  2. 2Row. One record. Hairline-separated, and the whole row tints on hover when it does something. Identity leads, the operational tail follows.
  3. 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.

NameValueStatusExpand
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

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.

StateSpecimenWhat changes
Rest
Record nameValue
Row titleValue
Row titleValue
Hairline-separated records on the card surface; the header band names the facts and never acts.
Row hover
Record nameValue
Row titleValue
Row titleValue
The whole row takes the faint wash when the row does something. A table whose rows do nothing does not hover.
Open
Record nameValueExpand
Row title Value

The detail the row was hiding.

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
Record nameValueExpand
Row title Value
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

Okafor ×2 Deposit pending
21–28 Feb, anniversary, surprise flag
DirectEmailA. Said
Do

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 / partyDatesStatusOwnerSource
Okafor ×221–28 Feb Deposit pending A. SaidEmail
Don't

Horizontal scroll hides columns behind an edge the agent cannot see. A dense table never scrolls sideways — it becomes cards instead.

Sheikha Al-Maktoum-Rothschild ×6
Ref ENQ-2026-08-1147
Do

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.

Sheikha Al-Maktoum-Rothschild ×6
Don't

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-expanded and an accessible name for both directions; the detail row is hidden when 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 by aria-expanded and 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>