Layout
Annotated transcript
Conversation bubbles where the fill is the speaker’s pale tint but body text stays at the page ink, the speaker’s strong color is a spine on the outer edge that flips sides for own messages, and per-message metadata lives in a fixed gutter beside the bubble.
Reach for it when building
- interview and Q&A articles
- support ticket transcripts
- annotated email or chat exchanges
- dialogue in long-form writing
- design critique threads
- conversation
- gutter
- status-roles
- contrast
- css-only
<div class="atx-thread">
<div class="atx-row">
<div class="atx-gutter">
<span class="atx-note">opened 20m later</span>
<b class="atx-verdict atx-fast">fast</b>
</div>
<div class="atx-bubble atx-them">
<span class="atx-name">Reviewer</span>
<p>Draft two is ready whenever you want a second pass — I left notes on the intro.</p>
</div>
</div>
<div class="atx-row atx-own">
<div class="atx-bubble atx-me">
<span class="atx-name">Editor</span>
<p>Thanks — I will fold the notes in tonight and send the final over.</p>
</div>
<div class="atx-gutter atx-gutter-own">
<span class="atx-note">never opened</span>
<b class="atx-verdict atx-never">never</b>
</div>
</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.
.atx-thread {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 640px;
}
.atx-row {
display: grid;
grid-template-columns: 8rem minmax(0, 1fr);
gap: 0.9rem;
align-items: start;
}
.atx-row.atx-own { grid-template-columns: minmax(0, 1fr) 8rem; }
/* The one thing that varies per message gets its own visual channel beside
the bubble, not a slot inside the meta line where it disappears. */
.atx-gutter {
font-size: 0.9375rem;
color: var(--dim);
padding-top: 0.35rem;
text-align: right;
}
.atx-gutter-own { text-align: left; }
.atx-note { display: block; }
.atx-verdict { display: block; font-weight: 600; }
.atx-fast { color: var(--ok); }
.atx-never { color: var(--bad); }
.atx-bubble {
border-radius: 12px;
padding: 0.9rem 1.1rem;
/* Long-read text never sits on saturated fill: the tint is the speaker's
color at low strength over the surface, and body copy stays at ink. */
color: var(--ink);
}
.atx-them {
background: color-mix(in srgb, var(--accent) 9%, var(--surface));
border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
border-left: 4px solid var(--accent);
}
.atx-me {
background: color-mix(in srgb, var(--ok) 9%, var(--surface));
border: 1px solid color-mix(in srgb, var(--ok) 30%, transparent);
border-right: 4px solid var(--ok);
justify-self: end;
max-width: 90%;
}
.atx-name {
display: block;
font-weight: 650;
font-size: 0.9375rem;
}
.atx-them .atx-name { color: var(--accent); }
.atx-me .atx-name { color: var(--ok); }
.atx-bubble p { margin: 0.15rem 0 0; }Paste this into an agent to rebuild the pattern from scratch.
Build a transcript layout for rendering a two-party conversation — an interview, a support exchange, an annotated email thread — with two rules doing all the work.
Rule one: the bubble fill is the speaker's color at low strength, but the body text stays at the page's normal ink color. Derive the fill with `color-mix(in srgb, <speaker-color> 9%, <surface>)` and the border at around 30% strength, so long-read text never sits on saturated fill. The speaker's full-strength color appears only in two small places: the name line, and a 4px spine drawn down the bubble's outer edge. For the "own" side of the conversation the spine flips from border-left to border-right, the row's grid columns mirror, and the bubble right-aligns — side plus spine plus hue triple-encodes who is speaking, so no single channel carries it alone.
Rule two: per-message metadata (time to reply, read state, an annotation) lives in a fixed-width gutter beside each bubble, on the outer side, mirrored for own messages — never crammed into a meta line inside the bubble. The gutter holds a dimmed sentence plus a one-word bold verdict colored by a status role token: a good state in the success color, a bad state in the danger color. Because the verdict is also a word, color never carries the meaning alone.
Structure each row as a two-column grid (`8rem` gutter, `minmax(0, 1fr)` bubble) so gutters align vertically down the page and bubbles cannot force the column wider. Keep a fixed internal order inside the bubble — name, body, then anything else — so bubble shape stays predictable.
Every color comes from theme custom properties via color-mix, and both parties' tints have to stay legible in light and dark themes.