Data display
Shape-mark sequence
A sequence compressed into a row of small marks whose shape carries the state, in two forms — gems on a thread for a bounded ordered track, flat marks with a “+N” tail for an open-ended exchange — with neither ever drawing furniture that asserts something untrue.
Reach for it when building
- multi-part series navigation
- wizard and onboarding steps
- course module sequences
- quest or level progression
- thread previews in an inbox list
- comment activity in listing rows
- review back-and-forth summaries
- conversation shape in search results
- stepper
- sparkline
- series
- truncation
- css-only
- no-assets
<div class="sms-demo">
<section class="sms-block">
<p class="sms-label">Bounded and ordered — gems on a thread</p>
<ul class="sms-strip sms-joined">
<li class="sms-gem sms-done"><span>1</span></li>
<li class="sms-gem sms-done"><span>2</span></li>
<li class="sms-gem sms-current"><span>3</span></li>
<li class="sms-gem sms-locked"><span>4</span></li>
</ul>
<p class="sms-caption">Part 3 of 4 — accent fill is done, signal fill is you-are-here</p>
</section>
<section class="sms-block">
<p class="sms-label">Open-ended and two-party — flat marks with a tail</p>
<div class="sms-run" title="14 exchanges drawn in order; 6 more not drawn. Filled marks are yours, hollow marks are theirs; the ringed mark is a branch with two replies.">
<i class="sms-mark"></i><i class="sms-mark sms-hollow"></i><i class="sms-mark sms-hollow"></i><i class="sms-mark"></i><i class="sms-mark sms-branch"></i><i class="sms-mark sms-hollow"></i><i class="sms-mark"></i><i class="sms-mark"></i><i class="sms-mark sms-hollow"></i><i class="sms-mark"></i><i class="sms-mark sms-hollow"></i><i class="sms-mark sms-hollow"></i><i class="sms-mark"></i><i class="sms-mark sms-hollow"></i>
<span class="sms-more">+6</span>
</div>
<div class="sms-legend">
<span><i class="sms-mark"></i> yours</span>
<span><i class="sms-mark sms-hollow"></i> theirs</span>
<span><i class="sms-mark sms-branch"></i> branch</span>
</div>
</section>
</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.
.sms-demo { display: flex; flex-direction: column; gap: 1.75rem; }
.sms-block { display: flex; flex-direction: column; gap: 0.5rem; }
.sms-label {
margin: 0;
font-family: var(--mono);
font-size: 0.8125rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--dim);
}
/* ---------------------------------------------------------------
Form one — bounded ordered track.
--------------------------------------------------------------- */
.sms-strip {
display: flex;
gap: 0.75rem;
width: fit-content;
position: relative;
list-style: none;
margin: 0;
padding: 0.5rem;
}
/* The thread the gems hang on. Rendered only via the "joined" class, added
when there are 2+ stages: a single gem on a line reads as a broken strip,
not a short one. */
.sms-joined::before {
content: '';
position: absolute;
left: 0.9rem;
right: 0.9rem;
top: 50%;
height: 2px;
background: color-mix(in srgb, var(--dim) 55%, transparent);
}
.sms-gem {
z-index: 1;
width: 2rem;
height: 2rem;
rotate: 45deg;
border-radius: 5px;
border: 2px solid var(--dim);
background: var(--surface);
display: grid;
place-items: center;
}
/* Numbers counter-rotate so the digit stays upright inside the gem. */
.sms-gem span {
rotate: -45deg;
font-family: var(--mono);
font-size: 0.875rem;
font-weight: 700;
color: var(--ink);
}
.sms-done {
background: var(--accent);
border-color: color-mix(in srgb, var(--accent) 70%, var(--ink));
}
.sms-done span { color: var(--accent-ink); }
.sms-current {
background: var(--signal);
border-color: color-mix(in srgb, var(--signal) 70%, var(--ink));
}
.sms-current span { color: var(--surface); }
.sms-locked { opacity: 0.6; }
.sms-caption { margin: 0; font-size: 0.9375rem; color: var(--dim); }
/* ---------------------------------------------------------------
Form two — open-ended two-party run.
--------------------------------------------------------------- */
.sms-run {
display: inline-flex;
align-items: center;
gap: 4px;
cursor: help;
width: fit-content;
}
.sms-mark {
display: inline-block;
width: 8px;
height: 20px;
border-radius: 2px;
background: var(--accent);
}
/* Hollow = the other party: fill drops to a tint and the identity moves to
the border, so the two states differ in structure, not just hue. */
.sms-hollow {
background: color-mix(in srgb, var(--accent) 12%, var(--surface));
border: 2px solid var(--accent);
width: 4px;
height: 16px;
}
.sms-branch { box-shadow: 0 0 0 2px var(--signal); }
.sms-more {
font-size: 0.875rem;
color: var(--dim);
margin-left: 0.35rem;
font-variant-numeric: tabular-nums;
}
.sms-legend {
display: flex;
gap: 1.25rem;
font-size: 0.9375rem;
color: var(--dim);
align-items: center;
margin-top: 0.25rem;
}
.sms-legend span { display: inline-flex; align-items: center; gap: 0.4rem; }Paste this into an agent to rebuild the pattern from scratch.
Build a compressed view of a sequence as a row of small marks, one mark per item in order, where the mark's shape carries its state. Two rules govern both forms of it, and they are the reason to reach for this rather than a list.
**Encode every state at least twice.** Fill, border, size, and opacity are all available; color alone is not enough. Two marks that differ only in hue are two marks a colorblind reader cannot tell apart, and a strip is too small to carry a text label per item.
**Never draw furniture that asserts something untrue.** Every connective or terminal mark in this pattern is a claim about the data, and each one has a guard below. This is the discipline the pattern exists to teach; the shapes are just how it is expressed.
Which form you build depends on whether the sequence is bounded.
**A bounded, ordered track — a multi-part series, a wizard, a level path — becomes gems on a thread.** Each stage is a 2rem square with rotate: 45deg and a small border-radius so it reads as a cut gem rather than a plain diamond, with the number inside counter-rotated -45deg to stay upright, set in the mono face and centred with display: grid; place-items: center. The thread is a 2px absolutely-positioned line through the vertical centre of the list, inset from both ends so it starts and stops under the outermost gems, with the gems given z-index: 1 to sit on top of it.
Three states, each encoded twice: done is filled with the accent, its border darkened by mixing the accent toward ink, its number in the accent's contrast colour; current is filled with the signal colour, so "you are here" is a different hue from "behind you"; locked keeps the neutral outline and drops to opacity: 0.6. Below the strip, a caption states the position in words — "Part 3 of 4" — for anyone who cannot or does not read the gems. Use a real ul/li with list styling stripped, since the content is a sequence.
The guard here is the thread: render it only when there are two or more stages, gated behind a class the component adds conditionally. A single gem with a line through it reads as a broken strip rather than a short one.
**An open-ended two-party exchange — a message thread, a review cycle, a run of turns — becomes flat marks with a tail.** One party's marks are filled solid in the accent; the other party's are hollow, the fill dropping to a 12% tint with the accent moving to a 2px border and the mark made slightly narrower, so the difference survives in monochrome. An item with special structure — a branch, a fork, a flagged turn — keeps its fill and gains a ring drawn with box-shadow: 0 0 0 2px in the signal colour, which costs no layout. Build it as flexbox with a 4px gap over empty inline elements, so the whole thing is a dozen lines of CSS and renders anywhere, including inside a table row.
The guard here is the tail: past a mark limit, stop drawing and append a dimmed "+N" counter in tabular figures. A strip that simply ends asserts the sequence ended there, which is a false statement made by omission.
For the run, put the narration in a title tooltip on the strip — "14 exchanges drawn in order; 6 more not drawn; filled marks are yours…" — and set cursor: help as the affordance that a narration exists. That is the right trade at this size; ARIA gymnastics on a dozen empty spans is not. Ship a small legend beneath built from the identical mark classes, never from lookalike elements, or the legend drifts out of sync the first time someone restyles the marks.
Every colour comes from theme custom properties, and both the gem fills and the hollow-versus-filled distinction have to stay readable in light and dark themes.