Detail & stats

What comes after the table. Every row leads to a record's own page, and every dashboard opens with a row of numbers - until now the system stopped at the table and both were rebuilt by hand in every application.

Reference

sk-dl

A real <dl>, not a two-column table. These are facts about one record rather than rows of the same shape, so a screen reader reads "Status, Active" instead of announcing a table with one row and no headers. The <div> around each pair is valid - the spec allows dl > div > dt,dd precisely so a pair can be laid out as a unit - and without it the term and its value are two unrelated grid children that come apart the moment one wraps.

<dl class="sk-dl" data-layout="side">
  <div class="sk-dl-row">
    <dt class="sk-dt">Status</dt>
    <dd class="sk-dd">Active</dd>
  </div>
</dl>
AttributeTypeDefaultNotes
data-layout"side"stackedTerm in a leading column. Better for scanning short values, worse as soon as one is a paragraph - and it collapses back to stacked below 30rem, because two columns in a phone-width pane leave about eight characters for the value.
data-divided"true"-A rule between pairs, never above the first or below the last. For long lists, where the eye needs help keeping a value with its label.
data-numeric"true"-On the dd. Tabular figures, for the same reason the table has them: proportional digits make a column that cannot be compared by eye.

sk-stat

DIRECTION AND TONE ARE DIFFERENT THINGS, and conflating them is the classic bug here. Up is not good: churn up is bad, refunds up are bad, latency up is bad. So the arrow is chosen by direction and the colour by tone, and they are separate attributes. The arrow is a glyph rather than a colour change, because a red 12.5% and a green 12.5% are the same string to a reader who cannot separate them.

<div class="sk-stat">
  <p class="sk-stat-label">Revenue</p>
  <p class="sk-stat-value">$12,480</p>
  <p class="sk-stat-delta" data-tone="positive">↑ 12.5% <span class="sk-stat-caption">vs last month</span></p>
</div>
AttributeTypeDefaultNotes
data-tone"positive" | "negative" | "neutral"neutralThe colour. The binding defaults it from the direction, so a metric where up is bad says so once.
data-size"sm" | "lg"mdThe value's type size. The label and delta do not change - a smaller tile is a smaller number, not smaller everything.
sk-stat-groupwrapper-auto-fit grid: four across down to one with no media query. The one arrangement in this system that genuinely needs grid rather than the flex utilities - a flex row leaves the last tile stretched across its own line.
data-divided"true"-On the group. Replaces the gaps with rules, which is what makes four tiles read as one panel. The rule IS the gap - a 1px gap over the group's background - because with auto-fit, which tile starts a row is not knowable in CSS.

Examples

Stat row

Resize the preview: four across, then two, then one.

Players

12,480

12.5% vs last month

Sessions

48,201

3.1% vs last month

Avg. session

18m 40s

0% vs last month

Crashes

37

22% vs last month

Show markup
<div class="sk-stat-group">
  <div class="sk-stat">
    <p class="sk-stat-label">
      Players
    </p>
    <p class="sk-stat-value">
      12,480
    </p>
    <p class="sk-stat-delta" data-tone="positive">
      <svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
        <path d="m18 15-6-6-6 6">
        </path>
      </svg>
      12.5%
      <span class="sk-stat-caption">
        vs last month
      </span>
    </p>
  </div>
  <div class="sk-stat">
    <p class="sk-stat-label">
      Sessions
    </p>
    <p class="sk-stat-value">
      48,201
    </p>
    <p class="sk-stat-delta" data-tone="positive">
      <svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
        <path d="m18 15-6-6-6 6">
        </path>
      </svg>
      3.1%
      <span class="sk-stat-caption">
        vs last month
      </span>
    </p>
  </div>
  <div class="sk-stat">
    <p class="sk-stat-label">
      Avg. session
    </p>
    <p class="sk-stat-value">
      18m 40s
    </p>
    <p class="sk-stat-delta" data-tone="neutral">
      0%
      <span class="sk-stat-caption">
        vs last month
      </span>
    </p>
  </div>
  <div class="sk-stat">
    <p class="sk-stat-label">
      Crashes
    </p>
    <p class="sk-stat-value">
      37
    </p>
    <p class="sk-stat-delta" data-tone="positive">
      <svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
        <path d="m6 9 6 6 6-6">
        </path>
      </svg>
      22%
      <span class="sk-stat-caption">
        vs last month
      </span>
    </p>
  </div>
</div>

In a card

Divided, so it reads as one panel of numbers rather than four cards that happen to be adjacent.

Builds

412

Passing

398

Failed

14

Show markup
<div class="sk-card">
  <div class="sk-stat-group" data-divided="true">
    <div class="sk-stat" data-size="sm">
      <p class="sk-stat-label">
        Builds
      </p>
      <p class="sk-stat-value">
        412
      </p>
    </div>
    <div class="sk-stat" data-size="sm">
      <p class="sk-stat-label">
        Passing
      </p>
      <p class="sk-stat-value">
        398
      </p>
    </div>
    <div class="sk-stat" data-size="sm">
      <p class="sk-stat-label">
        Failed
      </p>
      <p class="sk-stat-value">
        14
      </p>
    </div>
  </div>
</div>

Up is not always good

Same direction, opposite tone. The second tile is the reason tone is an attribute.

Revenue

$12,480

12.5% vs last month

p99 latency

840ms

12.5% vs last month

Show markup
<div class="sk-stat-group">
  <div class="sk-stat">
    <p class="sk-stat-label">
      Revenue
    </p>
    <p class="sk-stat-value">
      $12,480
    </p>
    <p class="sk-stat-delta" data-tone="positive">
      <svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
        <path d="m18 15-6-6-6 6">
        </path>
      </svg>
      12.5%
      <span class="sk-stat-caption">
        vs last month
      </span>
    </p>
  </div>
  <div class="sk-stat">
    <p class="sk-stat-label">
      p99 latency
    </p>
    <p class="sk-stat-value">
      840ms
    </p>
    <p class="sk-stat-delta" data-tone="negative">
      <svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
        <path d="m18 15-6-6-6 6">
        </path>
      </svg>
      12.5%
      <span class="sk-stat-caption">
        vs last month
      </span>
    </p>
  </div>
</div>

Description list

Stacked, which is the default and the one that survives a long value.

Build 412

Status
Passing
Commit
350da8f Add the deploy script
Duration
4m 12s
Triggered by
Ada Lovelace Ada Lovelace
Show markup
<div class="sk-card">
  <div class="sk-card-header">
    <h3 class="sk-card-title">
      Build 412
    </h3>
  </div>
  <div class="sk-card-body">
    <dl class="sk-dl">
      <div class="sk-dl-row">
        <dt class="sk-dt">
          Status
        </dt>
        <dd class="sk-dd">
          <span class="sk-badge" data-status="success">
            <svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
              <path d="M20 6 9 17l-5-5">
              </path>
            </svg>
            Passing
          </span>
        </dd>
      </div>
      <div class="sk-dl-row">
        <dt class="sk-dt">
          Commit
        </dt>
        <dd class="sk-dd">
          <code>
            350da8f
          </code>
          Add the deploy script
        </dd>
      </div>
      <div class="sk-dl-row">
        <dt class="sk-dt">
          Duration
        </dt>
        <dd class="sk-dd" data-numeric="true">
          4m 12s
        </dd>
      </div>
      <div class="sk-dl-row">
        <dt class="sk-dt">
          Triggered by
        </dt>
        <dd class="sk-dd">
          <span class="sk-avatar" data-size="xs">
            <span aria-hidden="true">
              AL
            </span>
            <span class="sk-visually-hidden">
              Ada Lovelace
            </span>
          </span>
          Ada Lovelace
        </dd>
      </div>
    </dl>
  </div>
</div>

Side by side

Divided as well. Below 30rem this becomes the stacked layout above.

Plan
Team
Seats
12 of 20
Renews
1 September 2026
Billing email
billing@softkiwi.games
Show markup
<dl class="sk-dl" data-layout="side" data-divided="true">
  <div class="sk-dl-row">
    <dt class="sk-dt">
      Plan
    </dt>
    <dd class="sk-dd">
      Team
    </dd>
  </div>
  <div class="sk-dl-row">
    <dt class="sk-dt">
      Seats
    </dt>
    <dd class="sk-dd" data-numeric="true">
      12 of 20
    </dd>
  </div>
  <div class="sk-dl-row">
    <dt class="sk-dt">
      Renews
    </dt>
    <dd class="sk-dd">
      1 September 2026
    </dd>
  </div>
  <div class="sk-dl-row">
    <dt class="sk-dt">
      Billing email
    </dt>
    <dd class="sk-dd">
      billing@softkiwi.games
    </dd>
  </div>
</dl>