Atomic chips
Flowing prose that contains atomic interruptions — a mention pill, a hashtag, an inline link — is the shape of every modern social UI. The awkward version breaks a chip in half when the paragraph wraps: @and on one line, rew on the next. The right version treats each chip as indivisible, letting the surrounding text wrap around it.
Drag the width. Watch the chips. They never split.
Mechanism
Pretext's public library exposes prepare() and prepareWithSegments(); rich inline is a composition pattern on top. We model the paragraph as an array of items — text runs and chips — and prepare each one individually at the right font. A chip's width is measured once and stored as a plain number.
On every width change, we walk the items with layoutNextLine(). For each text run we ask Pretext for the next line that fits the remaining width; for each chip we subtract its pre-measured width from the remaining budget. If a chip won't fit on the current line, we break — the text run resumes on the next line with a fresh width budget. The chip is never split because Pretext never sees its characters.
The result matches the browser's own line-breaking for the prose, while the atomic items ride alongside as inviolable pills.
Application
Every interface that mixes reading with structure:
- A social feed where
@handles,#tags, and inline links look like first-class typographic citizens — not fragments pretending to be words. - A task description with inline assignees and priority pills that wrap at narrow widths without losing their pill shape.
- A rich commit message where issue references (
PROJ-142) and reviewer mentions stay whole at any column width. - An AI chat transcript whose citations render as compact badges inside the answer text, not as footnotes at the end.
The common property: objects that carry their own visual identity survive reflow. The prose reshapes; the objects don't.
"That which is not good for the bee-hive cannot be good for the bees. Consider, each thing given, what part belongs to you and what part belongs to the whole; and when the whole moves, move with it, but do not pretend to be another part."
Marcus Aurelius, Meditations, Book VI (Long trans., 1862)Direct Claude
layoutNextLine()
"tags inline without splitting"
→
compose text runs + fixed-width items; break the line when a chip won't fit
"the pills ride inside the prose"
→
render each run as its own <span>, position rows with a per-line top offset
"badge wraps like a word, not a character"
→
reserve chipWidth + gap before calling layoutNextLine() for the next text run
import { prepareWithSegments, layoutNextLine } from '@chenglou/pretext';
// Each text run is prepared in its own font; each chip carries a measured width.
const items = [
{ kind: 'text', prepared: prepareWithSegments('Sit with ', BODY_FONT), /* ... */ },
{ kind: 'chip', width: 62, label: '@dogen', tone: 'mention' },
{ kind: 'text', prepared: prepareWithSegments(' and let ', BODY_FONT), /* ... */ },
{ kind: 'chip', width: 74, label: '#mountains', tone: 'tag' },
// ...
];
// On every width change, walk the items one line at a time.
function layoutRich(items, maxWidth) {
const lines = [];
let i = 0, textCursor = null;
while (i < items.length) {
const fragments = [];
let remaining = maxWidth;
while (i < items.length) {
const item = items[i];
if (item.kind === 'chip') {
if (item.width > remaining && fragments.length) break;
fragments.push({ ...item });
remaining -= item.width + GAP;
i++; textCursor = null;
} else {
const line = layoutNextLine(item.prepared, textCursor ?? LINE_START, remaining);
if (!line) { i++; textCursor = null; continue; }
fragments.push({ text: line.text, className: 'frag' });
remaining -= line.width;
if (cursorsEqual(line.end, item.endCursor)) { i++; textCursor = null; }
else { textCursor = line.end; break; }
}
}
if (!fragments.length) break;
lines.push({ fragments });
}
return lines;
}