Cards
tier 2 — classes
Three parts, all optional except the outer one: .card holds the border and
clips its children, .card-head is the title row, .card-body is
the padded content. There is no card component and no variant parameter — for a variant,
write the markup you need and take any colour from a token. All three share one gutter,
--card-pad-block / --card-pad-inline, which
data-density="compact" tightens — so a dense card list needs no rules of its
own.
--surface-here
— the colour of the surface
its content is sitting on. Anything that has to be opaque reads it and is right without naming
a surface (--timeline-surface, --group-head-bg,
--scroll-bg); on a surface your app paints, declare it once on that
element and everything inside follows.
Head and body
Group multiple right-hand items in .card-head-right so they share a gap.
Nightly maintenance: prunes stale orders and trims the run history.
<div class="card">
<div class="card-head">
<strong>Reconciliation</strong>
<div class="card-head-right">
<span class="badge badge-go">Enabled</span>
<button class="btn btn-sm">Run now</button>
</div>
</div>
<div class="card-body">
<p class="form-hint" style="margin-bottom:10px">
Nightly maintenance: prunes stale orders and trims the run history.
</p>
<div class="kv"><span class="k">Last run</span><span class="v">2026-07-30 03:15</span></div>
<div class="kv"><span class="k">Duration</span><span class="v">4.2 s</span></div>
<div class="kv"><span class="k">Rows removed</span><span class="v">1 284</span></div>
</div>
</div>
Body only
Drop the head when the content names itself.
No pending approvals
Everything in the queue has been decided. New requests appear here automatically.
<div class="card">
<div class="card-body">
<h2 style="margin:0 0 4px; font-size:14px">No pending approvals</h2>
<p class="form-hint">Everything in the queue has been decided. New requests appear here automatically.</p>
</div>
</div>
Body without padding
.card-body--flush drops the padding, for content drawn edge to edge inside the
card — a canvas, a chart, a map. It is the sibling of
.list-row--flush.
Relationships
The block above reaches the card's own edges. How tall it is stays the content's decision; the class only takes the padding away.
<div class="card" style="max-width:420px">
<div class="card-head"><h3>Relationships</h3></div>
<div class="card-body card-body--flush" style="height:9rem">
<div class="skeleton" style="height:100%; border-radius:0">
</div>
</div>
<div class="card-body">
<p class="form-hint">
The block above reaches the card's own edges. How tall it is stays the content's
decision; the class only takes the padding away.
</p>
</div>
</div>
A card in a card in a card
.card takes --surface-raised-1, a card inside one takes
-2 and a card inside that takes -3, with nothing in the markup
saying which level it is; a fourth level stays at the third. The levels resolve through the
theme's own base ramp, so Graphite takes all three neutral.
27 sources, two of them slower than their timeout allows.
Timed out after 30 s. Retried once, then skipped — the hint you are reading is the level this ladder stops at.
<div class="card">
<div class="card-head">
<strong>Run 4 812</strong>
<span class="badge badge-warn">1 step failed</span>
</div>
<div class="card-body sedna-col sedna-gap-2">
<div class="kv"><span class="k">Started</span><span class="v">08:14:02</span></div>
<div class="card">
<div class="card-head"><strong>Extract</strong></div>
<div class="card-body sedna-col sedna-gap-2">
<p class="form-hint" style="margin:0">
27 sources, two of them slower than their timeout allows.
</p>
<div class="card">
<div class="card-body">
<strong>src-db-14</strong>
<p class="form-hint" style="margin:6px 0 0">
Timed out after 30 s. Retried once, then skipped — the hint you are
reading is the level this ladder stops at.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
Head, body and foot
.card-foot is the row a card ends with — the actions that decide what
happens to the content above them. Use it instead of a .card-body holding
.form-actions, and put a note or a destructive action on
the far side with .card-foot--split.
The cataloguer found postgres and postgresql on 41 records
between them. Approving keeps the first and retags the rest.
Prunes stale orders and trims the run history.
<div class="sedna-col sedna-gap-2">
<div class="card">
<div class="card-head">
<strong>Merge two tags</strong>
</div>
<div class="card-body">
<p class="form-hint" style="margin-bottom:10px">
The cataloguer found <code>postgres</code> and <code>postgresql</code> on 41 records
between them. Approving keeps the first and retags the rest.
</p>
<div class="kv"><span class="k">Raised by</span><span class="v">cataloguer</span></div>
<div class="kv"><span class="k">Confidence</span><span class="v">0.94</span></div>
</div>
<div class="card-foot">
<input class="form-input form-input-sm" style="max-width:260px" type="text"
placeholder="Why, in one line" aria-label="Reason" />
<button class="btn btn-sm">Decline</button>
<button class="btn btn-go btn-sm">Approve</button>
</div>
</div>
<div class="card">
<div class="card-body">
<strong>Nightly reconciliation</strong>
<p class="form-hint" style="margin:6px 0 0">
Prunes stale orders and trims the run history.
</p>
</div>
<div class="card-foot card-foot--split">
<span class="form-hint">Last run 03:15, 4.2 s</span>
<button class="btn btn-sm">Run now</button>
</div>
</div>
</div>
A foot read left to right
.card-foot--start aligns the row to the leading edge, for a work card rather
than a form: the primary action first and its secondaries after it, with one trailing control
pushed to the far end by .sedna-push. It is also what a status line in place of
the actions needs — right-aligned text reads as a mistake. Use --split only for
two groups, because it spreads whatever it holds across the whole card.
Northwind Retail returned four of six items. The refund waits on this.
Same queue, already taken by somebody else.
<div class="sedna-col sedna-gap-2" style="max-width:520px">
<div class="card">
<div class="card-body">
<strong>Credit note for order 10482</strong>
<p class="form-hint" style="margin:6px 0 0">
Northwind Retail returned four of six items. The refund waits on this.
</p>
</div>
<div class="card-foot card-foot--start">
<button class="btn btn-go btn-sm">Approve</button>
<button class="btn btn-sm">Send back</button>
<button class="btn btn-ghost btn-sm sedna-push" aria-label="More actions">
<i class="ri-more-2-fill"></i>
</button>
</div>
</div>
<div class="card">
<div class="card-body">
<strong>Credit note for order 10488</strong>
<p class="form-hint" style="margin:6px 0 0">
Same queue, already taken by somebody else.
</p>
</div>
<div class="card-foot card-foot--start">
<span class="text-muted">
<i class="ri-lock-line"></i> View only — claimed by Alex Fischer
</span>
</div>
</div>
</div>
Sections inside one body
.card-section divides sub-sections of one body with a hairline and the card's
own spacing. The rule is drawn only above a section that has something before it, so a
section you do not render takes its divider with it. Prefer it to a second
.card-body, which ends the first body's layout at the seam, and to a
.divider, whose margins are sized for a page.
Two files, both added when the hold was raised.
Claimed by Alex Fischer, four minutes ago.
<div class="card" style="max-width:520px">
<div class="card-head"><strong>Reservation 10482</strong></div>
<div class="card-body">
<div class="card-section">
<div class="kv"><span class="k">Customer</span><span class="v">Northwind Retail</span></div>
<div class="kv"><span class="k">Held until</span><span class="v">2026-08-02 18:00</span></div>
</div>
<div class="card-section">
<strong>Attachments</strong>
<p class="form-hint" style="margin:6px 0 0">
Two files, both added when the hold was raised.
</p>
</div>
<div class="card-section">
<strong>History</strong>
<p class="form-hint" style="margin:6px 0 0">
Claimed by Alex Fischer, four minutes ago.
</p>
</div>
</div>
</div>
A table in a card
No .card-body — the table brings its own cell padding.
| Order | Action | By | When |
|---|---|---|---|
| ORD-4209 | Approved | Alex Fischer | 4 min ago |
| ORD-4204 | Taken over | J. Weber | 22 min ago |
| ORD-4198 | Rejected | Alex Fischer | 1 h ago |
<div class="card">
<div class="card-head">
<strong>Recent decisions</strong>
<span class="badge">3</span>
</div>
<table class="table">
<thead>
<tr><th>Order</th><th>Action</th><th>By</th><th>When</th></tr>
</thead>
<tbody>
<tr><td>ORD-4209</td><td><span class="badge badge-go">Approved</span></td><td>Alex Fischer</td><td class="col-muted">4 min ago</td></tr>
<tr><td>ORD-4204</td><td><span class="badge badge-warn">Taken over</span></td><td>J. Weber</td><td class="col-muted">22 min ago</td></tr>
<tr><td>ORD-4198</td><td><span class="badge badge-danger">Rejected</span></td><td>Alex Fischer</td><td class="col-muted">1 h ago</td></tr>
</tbody>
</table>
</div>
Key/value rows
.kv pushes label and value apart; the value takes the monospace token, so
numbers and IDs align down the column.
<div class="card" style="max-width:380px">
<div class="card-body">
<div class="kv"><span class="k">Assignment group</span><span class="v">EU-Central</span></div>
<div class="kv"><span class="k">Priority</span><span class="v">P3</span></div>
<div class="kv"><span class="k">SLA remaining</span><span class="v">01:42:18</span></div>
<div class="kv"><span class="k">Confidence</span><span class="v">0.87</span></div>
</div>
</div>
A guarded card
A card the reader can read but must claim, accept or unlock before acting on.
.guard is absolute, so the card needs position: relative; put
inert on the guarded content, or it stays in the tab order behind the scrim.
The shared mailbox has 200 MB left. The customer has replied twice.
Claim this order to read the customer's reply and answer on the team's behalf.
<div class="card" style="position:relative; max-width:420px">
<!-- inert, because legible is not the same as reachable: without it the whole
card stays in the tab order behind a scrim nobody can type through. -->
<div inert>
<div class="card-head">
<strong>Mailbox quota exceeded on EXCH-02</strong>
<div class="card-head-right"><span class="badge badge-sev-2">P2</span></div>
</div>
<div class="card-body">
<p class="form-hint" style="margin-bottom:10px">
The shared mailbox has 200 MB left. The customer has replied twice.
</p>
<div class="kv"><span class="k">Raised</span><span class="v">2026-08-28 07:41</span></div>
<div class="kv"><span class="k">Customer</span><span class="v">Contoso Logistics</span></div>
</div>
</div>
<div class="guard">
<div class="guard-card">
<span class="guard-icon"><i class="ri-lock-line" aria-hidden="true"></i></span>
<p>Claim this order to read the customer's reply and answer on the team's behalf.</p>
<button class="btn btn-primary" type="button">Claim</button>
</div>
</div>
</div>
A guarded card over taller content
The prompt is centred, not stretched: over a card taller than it, the guard card hovers in the middle and the blurred content stays visible above and below it.
Claim this order to read the customer's reply and answer on the team's behalf.
<div class="card" style="position:relative; max-width:420px">
<!-- inert, because legible is not the same as reachable: without it the whole
card stays in the tab order behind a scrim nobody can type through. -->
<div inert>
<div class="card-head">
<strong>Mailbox quota exceeded on EXCH-02</strong>
<div class="card-head-right"><span class="badge badge-sev-2">P2</span></div>
</div>
<div class="card-body">
<div class="kv"><span class="k">Raised</span><span class="v">2026-08-28 07:41</span></div>
<div class="kv"><span class="k">Customer</span><span class="v">Contoso Logistics</span></div>
<div class="kv"><span class="k">Queue</span><span class="v">Exchange / Storage</span></div>
<div class="kv"><span class="k">Assignee</span><span class="v">unassigned</span></div>
<div class="kv"><span class="k">Last reply</span><span class="v">2026-08-29 16:02</span></div>
<div class="kv"><span class="k">SLA</span><span class="v">breaches in 3 h 40 m</span></div>
<div class="kv"><span class="k">Ticket</span><span class="v">TCK-204471</span></div>
</div>
<div class="card-foot">
<button class="btn" type="button">Snooze</button>
<button class="btn btn-primary" type="button">Answer</button>
</div>
</div>
<div class="guard">
<div class="guard-card">
<span class="guard-icon"><i class="ri-lock-line" aria-hidden="true"></i></span>
<p>Claim this order to read the customer's reply and answer on the team's behalf.</p>
<button class="btn btn-primary" type="button">Claim</button>
</div>
</div>
</div>
A caveat about the card's own content
.card-warning pins an amber strip to the card's bottom edge — a stale
figure, a degraded source, a partial result.
<div class="card" style="max-width:380px">
<div class="card-head"><strong>Open orders</strong></div>
<div class="card-body">
<div class="kv"><span class="k">P1</span><span class="v">2</span></div>
<div class="kv"><span class="k">P2</span><span class="v">11</span></div>
<div class="kv"><span class="k">P3</span><span class="v">64</span></div>
</div>
<div class="card-warning">
<i class="ri-time-line"></i>
<span>The warehouse last answered 14 minutes ago — these counts may lag.</span>
</div>
</div>
.card--fill lets the body take the spare height, so every foot
and .card-warning in the row ends on one line. It is shown on a whole dashboard
under Grid and layout.