← All patterns · Layout Updated
Hairline stat strip
A row of stat tiles joined by hairlines that are grid gap, not borders — the container paints the border color and the tiles paint the surface, so separators never double up or break when the row wraps — in a reporting form and in a second form where each tile is the filter for the list beneath it.
Reach for it when building
- a summary strip at the top of a detail page
- a dashboard KPI row
- an account usage overview
- report header figures
- a profile stats bandThe same strip turns into a filter when each tile is a toggle button; see the second preview.
The whole trick is that the 1px of container showing through the grid gap is the hairline.Why not borders?Real borders double where two tiles meet and leave a stray edge when auto-fit wraps the row. A gap physically cannot do either. Nothing else in the pattern needs explaining.
Reporting only — the figures are the whole job.
Same strip, working as the filter. Press a tile to narrow the list; press it again to clear.
- Bicycle drivetrain teardownDone
- Kiln firing, part threeDownloading
- Field recording — marsh at dawnQueued
- Lathe restoration, episode 12Done
- Sourdough hydration testsFailed
- Bookbinding: the long stitchQueued
Showing all 6 downloads.
<div class="hss-strip">
<div class="hss-tile"><span class="l">Documents</span><span class="v">1,482</span><span class="u">this period</span></div>
<div class="hss-tile"><span class="l">Median time</span><span class="v">3.2s</span><span class="u">last 24h</span></div>
<div class="hss-tile"><span class="l">Clean runs</span><span class="v">98.4%</span><span class="u">no retries</span></div>
<div class="hss-tile"><span class="l">Quality score</span><span class="v none" aria-label="No value">—</span><span class="u">not rated yet</span></div>
</div>Colors come from shared theme tokens — --surface, --ink, --border, --accent and friends — so this CSS carries no palette of its own. Use Runnable file to copy the tokens along with it.
.hss-strip {
display: grid;
gap: 1px;
background: var(--border);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
grid-template-columns: 1fr;
}
@container (min-width: 21rem) { .hss-strip { grid-template-columns: repeat(2, 1fr); } }
@container (min-width: 42rem) { .hss-strip { grid-template-columns: repeat(4, 1fr); } }
.hss-tile { background: var(--surface); padding: .8rem 1rem; min-width: 0; }
.hss-tile .l { font: .8125rem/1.3 ui-monospace, monospace; text-transform: uppercase; color: var(--dim); }
.hss-tile .v { font-size: 1.6rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.hss-tile[aria-pressed="true"] {
background: color-mix(in srgb, var(--accent) 14%, var(--surface));
}
// Filter form only. Counts come from the unfiltered set, never the visible rows.
tiles.forEach((tile) => {
tile.addEventListener('click', () => {
const wasPressed = tile.getAttribute('aria-pressed') === 'true'
tiles.forEach((other) => other.setAttribute('aria-pressed', 'false'))
const state = wasPressed ? 'all' : tile.dataset.state
if (!wasPressed && state !== 'all') tile.setAttribute('aria-pressed', 'true')
rows.forEach((row) => { row.hidden = state !== 'all' && row.dataset.state !== state })
})
})Build a strip of statistic tiles that reads as one joined instrument panel rather than a row of floating cards.
The separators between tiles must not be borders. Make the container a CSS grid with gap: 1px and give the container itself the border color as its background; each tile then paints the surface color on top. The 1px of container showing through the gap is the hairline. This is the entire pattern: real borders double where two tiles meet and leave a stray edge when auto-fit wraps the row, and a gap physically cannot do either.
The full prompt continues with sections on pinning column counts, tile anatomy, two rules of honesty (render a missing value as a dimmed em dash; keep units with the number), the filter form, computing counts from the unfiltered set, and using aria-pressed rather than tab roles.
Two rules of honesty
A missing value renders as a dimmed em dash, never as zero.See itThe Quality score tile above: —, “not rated yet”. Units stay with the number they measure, so 3.2s and 98.4% never lose their meaning when a tile wraps to a new row.
In the filter form, counts come from the unfiltered set. Pressing Done still shows Queued 2 and Failed 1, so the strip keeps working as a summary while it filters.Tiles are toggle buttons with aria-pressed, not tabs: nothing here is a tab panel.
Nearby patterns
Section rail
Sticky vertical rail on wide screens, wrapping tab row on narrow ones, from the same markup.
Filtered to zero
The three empty states a list actually has, written as three different screens.
Single-property color variants Updated
One custom property carries the hue; border, fill, spine and dot derive from it.