Cards
A surface with a border and padding, and the KPI variant built on top of it. Two grids lay them out so a dashboard needs no layout CSS of its own.
Card
_78-card is the surface; _78-card-title is the heading slot. Consecutive
cards space themselves, so a stack of them needs no margins (in a grid, the gap does it
instead).
Revenue held steady while support volume fell for the third month running.
<div class="_78-card">
<div class="_78-card-title">Monthly summary</div>
<p>Revenue held steady while support volume fell for the third month running.</p>
</div>
Head and foot
_78-card-head is a flex row for a title with a control beside it — it clears the
title's bottom margin for you. _78-card-foot is the closing row at the bottom, with a
top border above it.
The foot is full-bleed: it cancels the card's padding with negative margins and
re-applies its own, so its border reaches both edges and its bottom sits flush with the card
— exactly like the foot of a modal.
_78-card-foot-fill adds the modal foot's tinted --bg3 surface to it. A short
form fits in the foot too — see the
inline form.
Twelve people have access to this workspace.
A CSV of everything in the current view, including hidden columns.
<div class="site-demo-block">
<div class="_78-card">
<div class="_78-card-head">
<div class="_78-card-title">Team members</div>
<button class="_78-btn _78-btn-sm" type="button">Invite</button>
</div>
<p>Twelve people have access to this workspace.</p>
<div class="_78-card-foot">
<button class="_78-btn _78-btn-ghost _78-btn-sm" type="button">Manage roles</button>
<button class="_78-btn _78-btn-primary _78-btn-sm" type="button">Done</button>
</div>
</div>
<div class="_78-card">
<div class="_78-card-title">Export</div>
<p>A CSV of everything in the current view, including hidden columns.</p>
<div class="_78-card-foot _78-card-foot-fill _78-flex _78-flex-between">
<span class="_78-text-sm _78-text-dim">Last export was 4 days ago</span>
<button class="_78-btn _78-btn-primary _78-btn-sm" type="button">Export CSV</button>
</div>
</div>
</div>
The foot is a plain block, not a flex row: buttons inside it sit at the left in source order.
Reach for _78-flex, _78-flex-between or _78-grow when you
want them arranged, as the second card above does.
Clickable cards
_78-card-link turns a card into a link target: it lifts on hover and takes the accent
border. Put it on an <a> so it is keyboard reachable and announced as a link
— a div with a click handler is neither. The Overview page is a grid of these.
<a class="_78-card _78-card-link" href="#card">
<div class="_78-card-title">Billing</div>
<p class="_78-text-sm _78-text-dim" style="margin:0">Plans, invoices and payment methods.</p>
</a>
KPI stat card
_78-stat-card is the dashboard card: a label, a big number, a delta and a sparkline.
Every part is optional — a label and a number alone is a perfectly good stat card.
| Element | What it is |
|---|---|
_78-stat-card__head | Flex row holding the label and an optional icon or control slot. |
_78-stat-card__label | The small uppercase caption. |
_78-stat-card__icon | A tinted square for a small SVG. |
_78-stat-card__actions | A control slot at the end of the head — a small toggle beside the label. See below. |
_78-stat-card__num | The big number. |
_78-stat-card__row | Flex row for the delta and its caption. |
_78-stat-card__sub | The caption beside or under the number. |
_78-stat-card__spark | Sparkline slot — give it data-values. |
_78-stat-card__lines | Key : value lines. See below. |
_78-stat-delta | The signed change. See Data-viz. |
Number tones
The number takes a tone in its short form (_78-success, _78-danger …) in addition to _78-stat-card__num. These are the
semantic colors, so they mean what they always mean — they do not follow your accent.
<div class="_78-kpi-grid">
<div class="_78-stat-card">
<div class="_78-stat-card__head"><span class="_78-stat-card__label">Accent</span></div>
<div class="_78-stat-card__num _78-accent">$48,290</div>
</div>
<div class="_78-stat-card">
<div class="_78-stat-card__head"><span class="_78-stat-card__label">Success</span></div>
<div class="_78-stat-card__num _78-success">99.98%</div>
</div>
<div class="_78-stat-card">
<div class="_78-stat-card__head"><span class="_78-stat-card__label">Warn</span></div>
<div class="_78-stat-card__num _78-warn">14 days</div>
</div>
<div class="_78-stat-card">
<div class="_78-stat-card__head"><span class="_78-stat-card__label">Danger</span></div>
<div class="_78-stat-card__num _78-danger">3 failing</div>
</div>
</div>
A complete stat card
The delta's color comes from its sign. Add data-invert="true" where a rise is bad, which
flips the color but never the arrow — the arrow always tells the truth about direction.
<div class="_78-kpi-grid">
<div class="_78-stat-card">
<div class="_78-stat-card__head">
<span class="_78-stat-card__label">Revenue</span>
<span class="_78-stat-card__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M3 17l6-6 4 4 8-8"/></svg>
</span>
</div>
<div class="_78-stat-card__num _78-accent">$48,290</div>
<div class="_78-stat-card__row">
<span class="_78-stat-delta" data-delta="12.4"></span>
<span class="_78-stat-card__sub">vs last month</span>
</div>
<div class="_78-stat-card__spark" data-values="18,22,19,27,25,31,29,38,44,41,52" data-tone="auto"></div>
</div>
<div class="_78-stat-card">
<div class="_78-stat-card__head">
<span class="_78-stat-card__label">Support tickets</span>
</div>
<div class="_78-stat-card__num _78-success">312</div>
<div class="_78-stat-card__row">
<span class="_78-stat-delta" data-delta="8.2" data-invert="true"></span>
<span class="_78-stat-card__sub">up is bad here</span>
</div>
<div class="_78-stat-card__spark _78-tone-danger" data-values="20,22,24,23,27,29,31"></div>
</div>
</div>
A stat card can also be a link: combine it with _78-card-link and it takes the accent
border on hover.
<a class="_78-stat-card _78-card-link" href="/revenue">…</a>
A control beside the label
A number often has two readings — today or all-time, this month or this year. Put the switch in
_78-stat-card__actions at the end of the head. A
_78-seg inside it drops to a
compact 24px, and the slot keeps the head at the label's own height, so the card's number still lines
up with its neighbors' in a grid. An icon button works the same way.
<div class="_78-kpi-grid">
<div class="_78-stat-card">
<div class="_78-stat-card__head">
<span class="_78-stat-card__label">Revenue</span>
<div class="_78-stat-card__actions">
<div class="_78-seg" aria-label="Revenue period">
<button class="_78-seg-btn" type="button" data-value="month" aria-checked="true">Month</button>
<button class="_78-seg-btn" type="button" data-value="year">Year</button>
</div>
</div>
</div>
<div class="_78-stat-card__num">$48,290</div>
<div class="_78-stat-card__sub">this month</div>
</div>
<div class="_78-stat-card">
<div class="_78-stat-card__head">
<span class="_78-stat-card__label">Active seats</span>
</div>
<div class="_78-stat-card__num">1,204</div>
<div class="_78-stat-card__sub">of 1,500</div>
</div>
</div>
Keep the control out of _78-stat-card__label itself: the label is uppercase, letter-spaced
text, and that styling would reach the buttons.
Key : value lines
Sometimes the card is a label and several figures rather than one big number — the account's
change, how it did against a benchmark, how the trades did. _78-stat-card__lines holds
them one per row, each a _78-stat-card__key and a _78-stat-card__value. The
keys share one column, so every value starts on the same edge however long the keys are. In a value,
<b> is the figure (give it a short tone like _78-success) and
<small> is the dim aside after it.
<div class="_78-kpi-grid _78-kpi-grid-lg">
<div class="_78-stat-card">
<div class="_78-stat-card__label">Portfolio</div>
<div class="_78-stat-card__num">$48,290</div>
<div class="_78-stat-card__lines">
<div class="_78-stat-card__line">
<span class="_78-stat-card__key">Account</span>
<span class="_78-stat-card__value"><b class="_78-success">+$123.45</b> <small>today</small></span>
</div>
<div class="_78-stat-card__line">
<span class="_78-stat-card__key">vs benchmark</span>
<span class="_78-stat-card__value"><b class="_78-success">+0.50%</b> <small>you +1.2% · index +0.7%</small></span>
</div>
<div class="_78-stat-card__line">
<span class="_78-stat-card__key">Trades</span>
<span class="_78-stat-card__value"><b class="_78-danger">−0.30%</b> <small>on capital deployed, across 9 days</small></span>
</div>
</div>
</div>
</div>
| Class | What it does |
|---|---|
_78-stat-card__lines | The wrapper: a two-column grid, keys then values, --space-2 under the number. |
_78-stat-card__line | One line. Inside the wrapper it joins the grid; on its own it's a flex row. |
_78-stat-card__key | --fs-xs, --dim, never wraps. |
_78-stat-card__value | --fs-sm, --text, tabular figures; <small> inside is the dim aside. |
Meta line
_78-meta is the small dim line of facts under a headline — a scan summary, a thesis,
the factors behind a score. Write each fact as its own element and the kit draws the
· between them, so the markup stays a list of facts instead of a string with the
punctuation typed in. The separator has empty alt text, so a screen reader doesn't read it out.
Three new names are waiting for a first look.
<div class="_78-card">
<div class="_78-card-title">Watch list</div>
<p class="_78-meta">
<span class="_78-meta-label">Tonight's scan:</span>
<span>Scanned <b>1,234</b> names</span>
<span>read the top <b>20</b></span>
<span>added <b>3</b></span>
<span>passed <b>17</b></span>
</p>
<p>Three new names are waiting for a first look.</p>
</div>
Straight after a _78-card-title or _78-card-head it tucks up under the title
and keeps the title's spacing below itself, so it reads as part of the heading. A
_78-meta-label lead-in takes no separator after it, and a <b> lifts a
figure out of the dim text.
Clamped
A thesis or a list of factors can run long. _78-meta-clamp cuts the line at one with an
ellipsis, so the card never grows taller than its neighbors; _78-meta-clamp-2 allows
two, and --_78-meta-lines any number.
<div class="site-demo-block">
<p class="_78-meta _78-meta-clamp">
<span class="_78-meta-label">Watched for:</span>
<span>a pullback to the 50-day average on falling volume, then a reclaim of the prior week's high
before the earnings date, with the sector holding above its own 200-day line</span>
<a href="#meta">Open the watch list →</a>
</p>
<p class="_78-meta _78-meta-clamp" style="--_78-meta-lines: 2">
<span>Two lines with the custom property: renewal is due in twelve days, the champion moved teams last
month, usage is down a fifth since the summer, two open tickets mention the export limit, the last
invoice was paid eleven days late, the new procurement contact has not replied to either email, and the
account has asked twice about the price of the annual plan against the monthly one</span>
</p>
<p class="_78-meta _78-meta-xs">
<span>Updated <b>4 min</b> ago</span>
<span>3 sources</span>
<span>_78-meta-xs</span>
</p>
</div>
| Class / property | What it does |
|---|---|
_78-meta | --fs-sm, --dim, tabular figures. A · before every child after the first. |
_78-meta-label | A lead-in: --text, semibold, no separator after it. |
b / strong | A figure lifted out of the dim text. |
_78-meta-xs | --fs-xs — a tally line or a timestamp. |
_78-meta-clamp | One line, then an ellipsis. |
_78-meta-clamp-2 | Two lines, then an ellipsis. |
--_78-meta-lines | With a clamp class: any number of lines. |
Card stack
Sibling cards space themselves — but only siblings. When each card in a list is wrapped
(in a <form>, or by a template partial that brings its own element), or something hidden
sits between them, the + rule can't reach them and the list collapses into one slab.
_78-card-stack spaces its children with a gap instead, so it doesn't care what they are.
<div class="_78-card-stack">
<form>
<article class="_78-card">
<div class="_78-card-title">Northwind</div>
<p class="_78-text-dim">Renewal due in 12 days.</p>
</article>
</form>
<form>
<article class="_78-card">
<div class="_78-card-title">Contoso</div>
<p class="_78-text-dim">Waiting on a signed order form.</p>
</article>
</form>
</div>
| Class | What it does |
|---|---|
_78-card-stack | A one-column grid with a --space-4 gap. Any child is spaced, card or not; a hidden child takes no gap. |
_78-card-stack-sm | With it: a tighter --space-3 gap. |
Grids
Two auto-fit grids, so you rarely write grid CSS for a dashboard. Both wrap on their own and need no media queries.
| Class | Minimum column | Use for |
|---|---|---|
_78-kpi-grid | 190px | The default KPI row — two to six across. |
_78-kpi-grid-sm | 150px | More, narrower stats on one row. |
_78-kpi-grid-lg | 240px | Fewer, roomier stats — long numbers. |
_78-card-grid | 200px | General cards, not just stats. |
Both are presets of the general _78-grid,
so a card in either can take _78-col-span-2 or _78-col-full.
The size modifiers go with _78-kpi-grid, not instead of it:
<div class="_78-kpi-grid _78-kpi-grid-sm"> … </div>
<div class="_78-kpi-grid _78-kpi-grid-lg"> … </div>
<div class="_78-card-grid"> … </div>
Cards in a grid line up
Stacked cards space themselves with a top margin. Inside _78-card-grid,
_78-kpi-grid, _78-flex or _78-flex-between that margin is dropped,
because the container's gap already does the spacing — so every card in a row sits level with
the first.
Level with its neighbors.
No stacking margin in a grid.
The gap spaces the row.
<div class="_78-card-grid">
<div class="_78-card">
<div class="_78-card-title">North</div>
<p class="_78-text-dim">Level with its neighbors.</p>
</div>
<div class="_78-card">
<div class="_78-card-title">South</div>
<p class="_78-text-dim">No stacking margin in a grid.</p>
</div>
<div class="_78-card">
<div class="_78-card-title">West</div>
<p class="_78-text-dim">The gap spaces the row.</p>
</div>
</div>
_78-card-grid sat 16px low. If you
compensated with your own margin-top: 0, it can go. In a grid of your own the kit
can't tell, so zero the margin there yourself.