Where this lands natively
Pretext is a 15KB library that re-implements, in JavaScript, a pair of operations the browser already knows how to do internally: measure a run of text in a given font, and break it across lines at a given width. The reason it's a userland library at all is that the browser doesn't expose those operations. Pretext's whole existence is a patch over a missing primitive.
Below, the trajectory. On the left — what Pretext does today, with canvas measureText as its only real foothold into the font engine. On the right — what the proposed FontMetrics API (CSS Houdini drafts) would give us directly: structured metrics, not a library that reconstructs them. The right panel is illustrative, a mock of where the standards conversation is going.
measureText is what you get from the browser. Everything else — segmentation, glue rules, bidi, CJK keep-all, emoji graphemes, soft hyphens — Pretext rebuilds in JavaScript using the canvas widths as ground truth.Mechanism
Today the browser's text engine lives entirely behind the rendering pipeline. You can ask it to paint text (and you'll see what you get) but you can't ask it what the engine decided — not the cap height of the font, not the set widths of a segment, not where it will break a line at 320 pixels. Canvas's measureText(str) is the one narrow opening. It returns { width, actualBoundingBoxAscent, actualBoundingBoxDescent, ... } for a single run. That's enough for Pretext, because all of layout reduces to "what's the width of each segment?" — once you know that, line-breaking is arithmetic.
The FontMetrics API (a CSS Houdini draft) proposes exposing what the engine already knows. Font-level metrics (capHeight, xHeight, ascent, descent) would come back as structured objects. Paired proposals in the line-breaking space would let you ask the engine itself for wrapped lines — no canvas probe, no re-implementation of Unicode break classes in JS.
When those land — and "when" is the right word, not "if" — Pretext's 15KB shrinks to whatever polyfill glue is needed to round out browser coverage. The library doesn't become obsolete so much as reclassified: from "essential custom layout engine" to "pre-standards compatibility layer."
Application
The practical takeaway is about how you spend your budget today:
- Learn Pretext's API surface, not its implementation.
prepare+layoutis the shape the native API will eventually take, give or take the verb names. Understanding that shape is what transfers. - Ship Pretext now. The workarounds it provides — selectable DOM text with correct line breaks, canvas text that doesn't thrash, masonry grids without DOM reads — are worth shipping today, not "once browsers catch up." Browsers take years.
- Don't marry one library. If you've abstracted "ask for a paragraph's height at width W" behind an interface in your codebase, you can swap Pretext for a native call the day it ships. If you've inlined
prepare/layoutcalls everywhere, that's a migration.
References: FontMetrics Level 1 draft (CSS Houdini WG). Also note uWrap.js by @leeoniya — an ASCII-only alternative to Pretext, much simpler scope, mentioned in the HN discussion.
"The things which are impossible are not absolutely impossible, but only impossible to the degree that we do not yet know how to do them. What is hereafter standard was once the private invention of an unusual mind."
Contemplative paraphrase, after Ralph Waldo Emerson, Essays (1841)Direct Claude
// Write to this shape today, regardless of implementation.
// Tomorrow, swap Pretext for a native call behind the same interface.
interface Measurer {
prepare(text: string, font: string): Handle;
layout(h: Handle, width: number, lh: number): { height: number; lineCount: number };
}
// Today: Pretext.
import { prepare, layout } from '@chenglou/pretext';
const pretextMeasurer: Measurer = { prepare, layout };
// Future (illustrative — API not final):
// const nativeMeasurer: Measurer = {
// prepare: (text, font) => new CSS.FontFace(font).shape(text),
// layout: (h, w, lh) => h.breakLines({ width: w, lineHeight: lh }),
// };
// Your app talks to the interface, not the library.
export function paragraphHeight(m: Measurer, text: string, font: string, w: number, lh: number) {
const h = m.prepare(text, font);
return m.layout(h, w, lh).height;
}