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>| Attribute | Type | Default | Notes |
|---|---|---|---|
data-layout | "side" | stacked | Term 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>| Attribute | Type | Default | Notes |
|---|---|---|---|
data-tone | "positive" | "negative" | "neutral" | neutral | The colour. The binding defaults it from the direction, so a metric where up is bad says so once. |
data-size | "sm" | "lg" | md | The value's type size. The label and delta do not change - a smaller tile is a smaller number, not smaller everything. |
sk-stat-group | wrapper | - | 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
350da8fAdd 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>