Pretext: DOM-Free Text Measurement for JS
Pretext does one job well: it replaces getBoundingClientRect and DOM reflow with a prepare()/layout() split that turns text-height measurement into cached arithmetic. That's a real win for virtualized lists, auto-growing textareas, and shrinkwrapped chat bubbles. It's not a full CSS text engine yet, so exotic font features still need the DOM.
What is Pretext?
Pretext is a TypeScript library that measures and lays out multiline text without touching the DOM. A one-time prepare() pass segments the text and measures it with canvas measureText against the browser's own font engine; layout() then turns that into a height and line count with pure arithmetic — no getBoundingClientRect, no reflow.
Why avoid DOM text measurement?
Reading text height the normal way means calling getBoundingClientRect or offsetHeight, which forces the browser to run layout reflow — one of the more expensive things a browser does — every time you need a number for virtualization, an auto-growing textarea, or a resize handler. Pretext's own README frames this as the reason it exists: measure once with canvas against the real font engine, then treat every later width or resize as arithmetic instead of a fresh DOM read.
Core APIs: prepare, layout, and line walkers
- ✓prepare() runs the one-time work — normalize whitespace, segment the text, measure each piece with canvas — and returns an opaque handle you reuse.
- ✓layout() turns that handle into a height and line count with pure arithmetic, so it's cheap enough to call again on every resize.
- ✓prepareWithSegments() plus layoutWithLines() hands back the actual line strings, widths, and start/end cursors for manual rendering to canvas or SVG.
- ✓walkLineRanges() and measureLineStats() compute line count and max width without allocating any line text — handy for binary-searching a 'shrinkwrap' width.
- ✓layoutNextLineRange() streams one line at a time so the max width can change mid-paragraph, e.g. flowing text around a floated image.
- ✓A separate @chenglou/pretext/rich-inline entry point handles per-item fonts, atomic break: 'never' chips, and caller-owned extraWidth for pill chrome.
- ✓Soft hyphens inserted before prepare() become optional break points; a chosen break materializes as a trailing '-'.
- ✓setLocale() and clearCache() manage segmentation locale and shared caches for apps that cycle through many fonts.
Measuring text height without touching the DOM
Call prepare() once per text, font, and config, then reuse the result. It normalizes whitespace, segments the string, and measures each segment with canvas — the expensive work happens exactly once. Then call layout() on that handle whenever you need a height: ```ts import { prepare, layout } from '@chenglou/pretext' const prepared = prepare('Hello, world 你好', '16px Inter') const { height, lineCount } = layout(prepared, 320, 20) ``` layout() is cheap. Call it again on every resize — just don't call prepare() again for the same text and font, since that repeats the measurement pass. Pass { whiteSpace: 'pre-wrap' } to prepare() if you need textarea-style handling where spaces, tabs, and newlines stay visible. For manual line-by-line layout instead of just a height, swap in prepareWithSegments() and layoutWithLines(), which return the actual line strings and widths.
Installation
Install the package from npm: `npm install @chenglou/pretext`. That's the entire setup for using the library in your own app — no build step, config file, or bundler plugin is documented. If you want to see it work, the README describes a separate flow for running the bundled demos: clone the repo, run `bun install`, then `bun start` and open /demos/index in your browser (`bun run start:windows` on Windows). Prebuilt demos are also linked live from the README.
What Pretext does well
- ✓The prepare()/layout() split means the expensive segmentation-and-measurement pass runs once, and every later resize is just arithmetic.
- ✓Low-level line walkers (walkLineRanges, layoutNextLineRange) don't allocate line strings, so they're cheap enough for binary-searching a shrinkwrap width.
- ✓rich-inline is a dedicated, narrow API for chips and mentions instead of forcing you to fake atomic items with the paragraph API.
- ✓The example strings in the README mix Chinese, Arabic, and emoji in one call, and segLevels exists specifically for custom bidi rendering.
- ✓MIT-licensed with a small function surface — a handful of named exports, not a framework with its own config format to learn.
Caveats and unsupported features
- △Server-side rendering is listed as 'soon' in the README, not shipped — today it targets DOM, Canvas, and SVG in the browser only.
- △Automatic hyphenation isn't built in; you have to insert soft hyphens yourself before calling prepare().
- △system-ui is flagged as unsafe for layout() accuracy on macOS due to Chrome/Firefox bugs the README links to — you need a named font instead.
- △CSS text features outside the canvas font shorthand — font-optical-sizing, font-feature-settings, standalone font-variation-settings — aren't modeled.
- △Requires Intl.Segmenter and Canvas 2D text measurement at runtime, so older or nonstandard JS environments are unsupported.
Alternatives to Pretext
Frequently asked questions
Pretext targets the browser today — the README lists server-side rendering as 'soon,' not yet shipped. Its runtime needs Intl.Segmenter and Canvas 2D text measurement, which a bare Node process doesn't provide without a polyfill for both.
Pretext isn't Canvas/SVG-only — the README lists DOM, Canvas, and SVG as supported render targets right now, with server-side rendering marked as coming soon. The library itself only returns measurements and line data; you still draw the text with your own renderer.
Pretext segments and measures CJK and right-to-left text — its own README examples mix Chinese and Arabic in one string. The richer PreparedTextWithSegments handle also exposes a segLevels field for custom bidi-aware rendering, though the built-in line-breaking APIs don't read that field themselves.
Pretext's own caveats flag system-ui as unsafe for layout() accuracy on macOS, pointing to a platform bug ledger covering the underlying Chrome and Firefox issues. Use a named font like 'Inter' instead of system-ui when accuracy matters.
Calling getBoundingClientRect forces the browser to run layout reflow, one of the more expensive operations it performs, just to read back a size. Pretext skips that: prepare() measures text once via canvas against the browser's font engine, and layout() afterward is pure arithmetic.
Pretext supports both: prepare() and prepareWithSegments() accept a letterSpacing option (a CSS pixel value) and a wordBreak option covering normal and keep-all, matching CSS word-break behavior for CJK, Hangul, and no-space mixed-language text.
Best use cases
- •Sizing rows in a virtualized list or feed before they render, so scroll position doesn't jump when a guessed height turns out wrong.
- •Auto-growing textareas and chat composers that need pixel-accurate height as the user types, including preserved tabs and newlines via whiteSpace: 'pre-wrap'.
- •Dev-time or CI checks that a button or label won't overflow to a second line — no browser required to run the check.
- •Custom masonry-style or JS-driven layouts that need real paragraph line counts and widths instead of triggering CSS reflow to find out.
- •Flowing paragraph text around a floated image, where the available width per line shrinks until the image ends.
- •Shrinkwrapped UI like chat bubbles or tooltips that should size to the tightest width that still fits the text, via walkLineRanges().
Who should try it — and who should skip
Frontend engineers building virtualized lists, chat UIs, or auto-growing text inputs where a wrong pixel height causes layout shift are the target audience — especially if profiling already points at getBoundingClientRect as the hot path. Teams that need full CSS text rendering (variable font axes, font-feature-settings, RTL glyph-position accuracy) or that must run measurement in Node today should hold off; the README lists server-side as still coming, and several CSS features stay outside canvas's font shorthand.
Related repositories
Still deciding about pretext?
One click hands the question to an AI along with this page — see what it says about pretext.
