Key Takeaways: Pretext measures and lays out multiline text in pure JavaScript without ever touching the DOM, so you can finally build virtualization, masonry, shrink-wrap, and canvas typography without paying the cost of layout reflow.
What is Pretext?
If you have ever built a virtualized list, a masonry grid, or a rich chat UI, you already know the dirty secret of the web: measuring text is slow. Every call to getBoundingClientRect, offsetHeight, or clientWidth can force the browser to synchronously recompute layout, and once you do that inside a scroll loop, the frame budget collapses. Pretext is a pure JavaScript and TypeScript library that solves this problem by measuring and laying out multiline text without ever touching the DOM in the hot path.
Created by Cheng Lou (formerly of the React core team and Messenger), Pretext is published on GitHub at chenglou/pretext and has a live demo gallery at chenglou.me/pretext. It is framework-agnostic, has zero runtime dependencies, and supports rendering to the DOM, Canvas, SVG, WebGL, and eventually server-side.
What makes Pretext different is that it implements its own text measurement logic using the browser’s own font engine as ground truth. The result is browser-accurate width and height numbers – without triggering the expensive layout reflow that those measurements normally cause.

Why Pretext exists
The browser already lays out text, of course. But the moment you need to do anything beyond passive HTML – virtualize a long list, balance lines, shrink-wrap a tooltip, flow text around an image, build a masonry feed – you need to know the text’s dimensions before it is in the DOM. The traditional answers are all bad.
Render the text off-screen and measure it: triggers reflow, slow at scale. Estimate with character counts: inaccurate, especially for CJK and emoji. Cache measurements: caching invalidation is famously one of the hardest problems in computer science. Use the canvas measureText API directly: it gives you width for a single line but not multiline wrapping, hyphenation, bidi, or grapheme awareness.
Pretext provides the missing piece. It does the one-time analytic work once, returns an opaque handle, and then lets you query height, line counts, line widths, and per-line ranges as pure arithmetic. On resize, you re-run the cheap arithmetic part. No DOM. No reflow.
It also handles the things naive solutions get wrong – locale-aware grapheme segmentation via Intl.Segmenter, CJK and Hangul word-break: keep-all, soft hyphens, pre-wrap whitespace, tabs with default tab-size, letter-spacing, and a richer handle that exposes bidi levels for custom rendering.
Installing Pretext
Pretext ships as a single npm package with zero dependencies. Installation is one command.
Using npm or pnpm
npm install @chenglou/pretext
# or
pnpm add @chenglou/pretext
# or
yarn add @chenglou/pretextThat is the entire setup. You can immediately import prepare, layout, prepareWithSegments, and the manual-layout APIs from @chenglou/pretext. There is also a @chenglou/pretext/rich-inline submodule for inline rich-text flow with chips, mentions, and code spans.
Running the demos locally
If you want to explore the official demos, clone the repo and run them with Bun:
git clone https://github.com/chenglou/pretext.git
cd pretext
bun install
bun start
# On Windows, use:
# bun run start:windowsThen open /demos/index in your browser. You can also browse the same demos hosted online at chenglou.me/pretext, which is the fastest way to see what kind of layout work Pretext unlocks.
Runtime requirements
Pretext needs Intl.Segmenter and Canvas 2D text measurement. Every modern evergreen browser supports both. It does not require a build step beyond what your bundler already does for TypeScript or ES modules, and there is no native binary, no WebAssembly, and nothing platform-specific. The bundle is small and tree-shakable.
Using Pretext: the two main use cases
Pretext is intentionally split into two layers. Most apps will only need the first.
Use case 1 – measure paragraph height without touching the DOM
This is the killer feature for virtualization, masonry, and avoiding layout shift. The API is two functions.
import { prepare, layout } from '@chenglou/pretext'
const prepared = prepare('Your long paragraph text here.', '16px Inter')
const { height, lineCount } = layout(prepared, 320, 20)
// 320px max width, 20px line height
// Pure arithmetic. No DOM layout. No reflow.prepare() does the one-time work – normalize whitespace, segment via Intl.Segmenter, apply glue rules, measure segments via canvas, and return an opaque handle. layout() is the cheap hot path: pure arithmetic over cached widths. On window resize, only rerun layout(). Do not rerun prepare() for the same text; that defeats its precomputation.
You can pass options for whiteSpace: 'pre-wrap' (textarea-like, preserves spaces, tabs, and hard newlines), wordBreak: 'keep-all' (for CJK and Hangul mixed text), and letterSpacing in CSS pixels.
The returned height unlocks a long list of UI patterns that have always been awkward on the web – proper virtualization without guesstimates, real masonry layouts, balanced-text tooltips, scroll-anchor preservation when content loads asynchronously, and even development-time verification that a button label does not overflow.
Use case 2 – lay out the paragraph lines manually
If you are rendering to canvas, SVG, WebGL, or a custom layout engine, you need access to each individual line. Pretext gives you a richer handle plus an iterator-style API:
import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext'
const prepared = prepareWithSegments('Your paragraph', '18px "Helvetica Neue"')
const { lines } = layoutWithLines(prepared, 320, 26)
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i].text, 0, i * 26)
}For more advanced needs, walkLineRanges() and measureLineStats() return line counts and widths without allocating line text strings – perfect for binary-searching the tightest container width that still fits the paragraph (the long-missing multiline “shrink wrap” for the web).
For variable-width flows like text wrapping around a floated image, use layoutNextLineRange() to route text one line at a time:
let cursor = { segmentIndex: 0, graphemeIndex: 0 }
let y = 0
while (true) {
const width = y < image.bottom ? columnWidth - image.width : columnWidth
const range = layoutNextLineRange(prepared, cursor, width)
if (range === null) break
const line = materializeLineRange(prepared, range)
ctx.fillText(line.text, 0, y)
cursor = range.end
y += 26
}Rich inline text – chips, mentions, code spans
The @chenglou/pretext/rich-inline helper handles browser-like boundary whitespace collapse for sequences of inline items with different fonts. It keeps atomic items (chips, mentions) whole via break: 'never' and lets the caller own the chrome width (padding plus border) via extraWidth. It is intentionally narrow – inline-only, white-space: normal only, no nested markup – which keeps it predictable and fast.
Practical patterns Pretext unlocks
The combination of accurate measurement and a measurable cost lets you build patterns that have always been awkward on the web.
Virtualized lists with variable-height items. Predict each item’s height from its text content before it scrolls into view, with no caching trickery.
Balanced tooltips and toasts. Binary-search a max-width that produces visually pleasing line lengths.
Masonry and JS-driven flex. Decide layout positions from real text dimensions before the DOM ever renders them.
Canvas and SVG typography. Render text to any non-DOM target – useful for screenshots, server-side rendering, exporting graphics, and infinite-canvas apps.
Layout-shift-free async content. Pre-measure and reserve space so the page does not jump when text loads.
Caveats worth knowing
Pretext is not (yet) a full font rendering engine. It targets the common modern text setup: white-space: normal or pre-wrap, word-break: normal or keep-all, overflow-wrap: break-word, default tab-size: 8, and canvas font shorthand for everything you set in CSS. Automatic hyphenation is not built in – you insert soft hyphens yourself. Variable-font axes only affect layout when the active axis is reflected in the canvas font string (typically via weight). And avoid system-ui on macOS for layout accuracy; use a named font.
If your needs fit inside that envelope – and they almost always do – Pretext is the most practical way to escape DOM-reflow-driven text measurement today.
Final thoughts
Pretext is one of those rare libraries that quietly fixes a category of problems the entire web platform has worked around for years. By taking text measurement out of the layout phase and into pure JavaScript, it lets you build the next tier of UI – virtualized, balanced, shrink-wrapped, canvas-rendered, server-side-laid-out – without paying the reflow tax.
Star the GitHub repository, browse the live demos, and the next time you reach for getBoundingClientRect inside a scroll handler, reach for prepare and layout instead.








