Font as a string
Pretext uses the browser's own canvas font engine to measure. That means the font string you pass to prepare() is the contract between your application and Pretext's idea of width. Weight, style, size, family — if any of those drift from the CSS the browser paints with, the line breaks drift too.
Pick a face. The specimen restyles in CSS and is re-prepared against an identical font string. The line count and computed height update to match what the browser would produce.
A foolish consistency is the hobgoblin of little minds, adored by little statesmen and philosophers and divines. With consistency a great soul has simply nothing to do. Speak what you think now in hard words, and to-morrow speak what to-morrow thinks in hard words again, though it contradict every thing you said to-day.
Mechanism
The font string is exactly what you'd assign to canvasCtx.font: an optional weight (400, 600, …), an optional style (italic, oblique), a size (20px), and a family stack ('EB Garamond', serif). Pretext feeds that string to its measurement canvas. Every segment's width is whatever that configured canvas reports — which is also what the browser will render with.
When the font changes, the cached measurements are stale. You call prepare(text, newFont) again, get a fresh handle, and layout() it at your current width. The rule is plain: measure what you'll render. The font string on the page and the font string passed to prepare() must match in weight, style, size, and family — or the line breaks won't.
Pretext's README flags one quirk: system-ui is unsafe on macOS, because canvas and DOM can resolve to different faces. Use a named family you've loaded yourself.
Application
The unlock here is subtle but constant: you can honor the author's type choice inside a measured pipeline without losing the pipeline. Swap a display face, re-prepare, keep your layout math.
- A reading app where the reader picks a face — heights stay exact through every swap.
- A design system where body text lives in several weights; each weight is its own handle, reused across all widths.
- A pull-quote that goes italic on hover without a reflow flash — italic is just a different handle on the same text.
- A developer-time check: "this button label fits at
600 14px Inter, and only fits because of that" — testable, browser-free.
"A foolish consistency is the hobgoblin of little minds… Speak what you think now in hard words, and to-morrow speak what to-morrow thinks in hard words again."
Ralph Waldo Emerson, Self-Reliance (1841)Direct Claude
font string passed to prepare()
"measure what you'll render"
→
use the same font string on the element's CSS and in prepare()
"swap the face without re-measuring the world"
→
re-prepare() only the affected text; keep the layout pipeline
"italic on hover, no flash"
→
hold a second handle prepared with 'italic'; layout() it, then swap class
import { prepare, layout } from '@chenglou/pretext';
// One font string. Used for the CSS and for prepare() — in lock-step.
function setFont(fontStr, lineHeightRatio) {
specimen.style.font = fontStr;
// New face → stale measurements. Re-prepare.
const handle = prepare(TEXT, fontStr);
// Extract size from the font string so lineHeight stays in sync.
const sizePx = parseFloat(fontStr.match(/(\d+(?:\.\d+)?)px/)[1]);
const { height, lineCount } = layout(
handle, textRegion.clientWidth, sizePx * lineHeightRatio
);
textRegion.style.minHeight = height + 'px';
return { handle, height, lineCount };
}