Accent UI

Tables

The list a CMS lives in — sortable headers, row selection, row actions, and the empty state that stops a blank list reading as a failure.


The list a CMS lives in. Pages, records, fields, redirects — most admin surfaces are a list of something, so the skin claims exactly one table. mk-table is a real <table>, on purpose: rows are data, and the moment they become divs the reader loses column association and the browser loses its own table semantics.

Pages in this site
Address Status Updated Actions
About us /about Published 2026-08-24
Categories /categories Draft 2026-08-26
Summer hours /summer-hours Scheduled 2026-08-21

Write

<div class="mk-table-wrap">
  <table class="mk-table">
    <caption class="visually-hidden">Pages in this site</caption>
    <thead>
      <tr>
        <th scope="col" aria-sort="ascending">
          <button type="button" class="mk-table__sort">Title</button>
        </th>
        <th scope="col">Status</th>
        <th scope="col"><span class="visually-hidden">Actions</span></th>
      </tr>
    </thead>
    <tbody>
      <tr aria-selected="true">
        <th scope="row" class="fw-normal">About us</th>
        <td><span class="mk-status mk-status--live">Published</span></td>
        <td>
          <div class="mk-table__actions">
            <button type="button" class="au-btn">Edit</button>
            <button type="button" class="au-btn au-btn--danger">Delete</button>
          </div>
        </td>
      </tr>
    </tbody>
  </table>
</div>

The pieces

Piece Job
mk-table-wrap The frame — hairline border, app radius — and the overflow: a wide table scrolls inside it rather than pushing the page.
mk-table The table itself: control-size type, one density, hairline row rules, hover as a faint ink wash. No size suffixes exist to write.
caption (visually hidden) Names the list for assistive tech — "Pages in this site", not a table about nothing.
th scope="col" / th scope="row" Headers that say which axis they head. The row's name cell is a header.
mk-table__sort The click target inside a sortable header — see below.
mk-table__actions A row's verbs, right-aligned so they sit under each other down the column.
au-empty What renders when there are no rows — see below.

The name column's fw-normal takes the browser's default bolding off a th scope="row" — the one weight class markup gets to write, because it un-designs rather than designs. A column of semibold names would read as a design change, and this app does not get designed.

Sorting

A sortable header is a button inside the <th>, and the two split the job: the th carries aria-sort — which is what assistive tech reads — and the button carries the click.

<th scope="col" aria-sort="descending">
  <button type="button" class="mk-table__sort">Updated</button>
</th>
  • The sorted column's control wears accent ink, and the arrow is drawn by the skin from the attribute's value — markup never writes ↑ or ↓ itself, so the drawn state cannot disagree with the announced one.
  • Unsorted headers stay in the secondary voice with no arrow; hover raises them to ink.
  • Focus is the ring, as everywhere.

Selection

A selected row says so with aria-selected="true", and the skin draws the soft accent tint from the same attribute that announces it — state and its treatment cannot drift apart. Hover is a plain ink wash, so the two never read as each other: ink is the pointer passing, the accent is the app saying this one.

Status rides in a cell as mk-status — lifecycle said in a dot and a word, a label rather than a badge. Row actions are ordinary buttons at the one control height; delete wears the danger spelling and nothing else does.

Empty

An empty list still has to say something — a blank table reads as a failure to load. Keep the frame, swap the body:

<div class="mk-table-wrap">
  <div class="au-empty">
    No pages yet. <button type="button" class="au-btn au-btn--link">Create the first one</button>
  </div>
</div>
No pages yet.

One line: what would be here, and — when there is one — the single verb that starts it.

Don't

  • No divs playing rows — column association and the browser's own table semantics are the point of the element.
  • No Bootstrap .table, .table-sm, .table-hover — the claimed spelling is mk-table, and hover is already in it.
  • No bolding the name column back on — fw-normal on the row header is deliberate.
  • No hand-drawn sort arrows, no colour on the sorted column beyond what the skin gives — aria-sort is the single source of both.
  • No per-surface density tweaks — padding belongs to the skin, and a py-1 on a cell forks the app's rhythm at that one call site.