TopGit
GitHub Repo Review

Pretext: DOM-Free Text Measurement for JS

chenglou/pretext
PTopGit review image for chenglou/pretext
Review by Topgit.dev for chenglou/pretext, with GitHub repository stats and README context.
Quick verdict

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.

Stars
★ 50.8k
Forks
⑂ 2.8k
Language
TypeScript
License
See repository
Topic
UI / UX
Updated
Oct 2026
Homepage
GitHub

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.
How this repository's GitHub stars have grown over time. Source: star-history.com.View the star history ↗

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.
View on GitHub ↗Homepage ↗

Alternatives to Pretext

text-layout — chenglou's earlier project, credited in the README's Credits section as the design ancestor Pretext built forward from.Native DOM measurement (getBoundingClientRect/offsetHeight) — the default browser approach Pretext exists specifically to avoid, at the cost of triggering reflow on every read.Rolling your own canvas measureText caching layer — possible, but you lose Pretext's segmentation, bidi handling, and line-walking APIs and have to rebuild them.

Frequently asked questions

Does Pretext work on the server or in Node.js?

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.

Can Pretext render text to HTML or only Canvas/SVG?

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.

What happens with CJK languages and bidirectional RTL text in Pretext?

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.

Is Pretext safe to use with system-ui font on macOS?

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.

How does Pretext compare to calling getBoundingClientRect directly?

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.

Does Pretext support CSS letter-spacing or word-break options?

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

Source & attribution

Facts and API details sourced from the chenglou/pretext GitHub repository and its README (https://github.com/chenglou/pretext).

GitHub data · last synced Aug 14, 2026Reviewed by Henry
← Back to TopGit

Still deciding about pretext?

One click hands the question to an AI along with this page — see what it says about pretext.

GitHub