August 25, 2026 · By YasKad
chenglou/pretext

Pretext: measuring and laying out multiline text without touching the DOM

chenglou/pretext · 50,570★ · 2,739 forks

Everything worth knowing about chenglou/pretext: a JavaScript/TypeScript library that computes a paragraph’s height, line breaks, and ranges using the browser’s typography engine as a reference, without inserting or measuring DOM nodes on the hot path.

Disambiguation: this report documents chenglou/pretext (a text-measurement library, npm package @chenglou/pretext, created in March 2026). It should not be confused with PreTeXtBook/pretext (459 stars), an academic document authoring and publishing system, or with pretext-project/pretext-project.github.io, a collection of social-engineering pretexts.


What Pretext is

Pretext is a JavaScript/TypeScript library for measuring and laying out multiline paragraphs. It solves an operation that looks small but shapes many interfaces: knowing how many lines a piece of text will occupy and how tall it’ll be at a given width, without rendering it in the DOM and querying getBoundingClientRect() or offsetHeight, reads that force a layout re-derivation (reflow) — one of the most expensive operations in the browser.

The project splits two operations. prepare() does the work that depends on the text and the font — normalizing whitespace, segmenting with Intl.Segmenter, applying “glue” rules (NBSP, ZWSP, soft hyphens, hard breaks), and measuring segments with Canvas — and returns a reusable opaque handle. layout() takes that result, a max width, and a line height, and computes height and lineCount through arithmetic over cached widths. On a resize or in a virtualization pass, only the second part re-runs, not the whole analysis.

Pretext doesn’t try to replace the CSS layout engine or become a full font-rendering engine. It’s a specialized tool for when you need to predict or control a text flow from JavaScript: virtualization, Canvas, SVG, WebGL, dynamic editorial design, animation, dev-time label validation, and (coming soon, per the README) server-side rendering.

The origin

The repository was created on March 7, 2026 by Cheng Lou (chenglou), who, according to Simon Willison’s blog, was previously a React core developer and the original creator of the animation library react-motion (21,914 stars). The @chenglou/pretext package first published to npm on March 27, 2026 (version 0.0.0); the GitHub repository dates about twenty days earlier, suggesting private development before the public launch.

The README explicitly credits Sebastian Markbåge’s text-layout project as a predecessor (a fork is preserved, chenglou/text-layout, with 34 stars) and its architecture: Canvas’s measureText for shaping, pdf.js-based bidi data, and streaming line breaks. The README also describes a particular development method: the engine was iterated “very AI-favorable” by showing coding agents like Claude Code and Codex the browser’s ground truth and having them measure and iterate against it at every relevant container width, over the course of weeks.

Verification is a distinctive trait of the origin: tests rendered a complete copy of The Great Gatsby across several browsers to confirm the estimated measurements were correct, and later the corpora/ folder joined in, applying the same method to long public-domain documents in Thai, Chinese, Korean, Japanese, Arabic, and others.

Visually rich multilingual text layout scene: abstract glyph clusters representing Korean, Arabic RTL, Chinese, Thai, Japanese, and emoji characters flow across a dark cyberpunk canvas, bidirectional text segments curve around each other with neon directional arrows, line-break decisions are shown as glowing seams between graphemes, a faint open book in the background references long public-domain validation documents, the composition conveys accuracy across languages, complex scripts, mixed RTL/LTR content, and platform-specific emoji rendering, ultra-detailed, 8K resolution, dark mode, cyberpunk/tech aesthetic, neon accents, no legible text.

Philosophy and principles

Its core principle is separating what’s expensive from what’s repeatable: analyze and measure once, and recompute line breaks at many widths using cached data. The README warns in plain terms: “Don’t re-run prepare() for the same text and configuration; that would destroy its precomputation.” That’s the performance contract: prepare() is expensive but one-time; layout() is the hot path — sub-millisecond, with no DOM reads, no Canvas calls, and no string work.

Conceptual cyberpunk diagram of performance philosophy: one expensive preparation phase is shown as a dense glowing forge analyzing text once, while many cheap layout phases fan out into repeated neon line calculations across changing widths, a ghosted browser layout tree is bypassed entirely, reflow and layout thrashing are depicted as broken red circuits being avoided, the image emphasizes separation of costly analysis from repeatable distribution, browser font engine reference, and a deliberately narrow CSS-compatible scope, ultra-detailed, 8K resolution, dark mode, cyberpunk/tech aesthetic, neon accents, no legible text.

Another principle, visible in the code and internal documentation, is using the browser’s font engine as the reference rather than reinventing all of CSS. Pretext doesn’t separately model CSS properties that fall outside the canvas.font shortcut (like font-optical-sizing or font-feature-settings), and it limits its scope to common text configurations: white-space: normal and pre-wrap, word-break: normal and keep-all, overflow-wrap: break-word, line-break: auto, and letter-spacing as a numeric pixel value. That deliberate narrowness explains both its relative precision and its limits.

The platform-quirk correction approach — a bug “ledger” (PLATFORM_BUGS.md) linking to Chromium, Mozilla, and WebKit, with repros and capability-detected fixes — reflects a philosophy of honesty: rather than promising universal accuracy, it documents where it diverges from each browser and how that’s mitigated.

Cyberpunk engineering ledger visualization for platform bug tracking: a glowing holographic accounting book opens to reveal entries for Chromium, Mozilla, and WebKit quirks, each bug appears as a small neon chip with reproduction icons, mitigation shields, and capability-detection markers, faint browser engine logos are abstracted as wireframe circuit patterns, the overall mood is honest, precise, and systems-level, emphasizing documented divergence from real browsers rather than universal perfection, ultra-detailed, 8K resolution, dark mode, cyberpunk/tech aesthetic, neon accents, no legible text.

How it works

The browser computes an element’s final size within a tree of styles, fonts, containers, and flow rules. Querying geometry after DOM changes can force it to synchronize that work. For a handful of static labels it’s irrelevant; in large lists, streaming messages, animations, or many height corrections, alternating writes and reads produces layout thrashing and visible jank.

Pretext sidesteps the layout tree for estimation. It uses CanvasRenderingContext2D.measureText(), which returns text metrics per the active font, as its measurement reference; it then composes lines in user code. The advantage isn’t that Canvas is magically faster than all of CSS, but that layout()’s hot path doesn’t need to insert a temporary element or ask the DOM for geometry.

Detailed cyberpunk visual representing the prepare stage of a text measurement library: a stream of abstract glyph-like characters enters a glowing pipeline, passing through a segmented grapheme splitter, space normalization nodes, and canvas measurement probes, tiny neon markers represent non-breaking spaces, zero-width spaces, soft hyphens, and hard line breaks, an opaque reusable handle emerges as a compact glowing token, background filled with faint font metrics, UTF-16 code units, and Intl.Segmenter-like grid patterns, ultra-detailed, 8K resolution, dark mode, cyberpunk/tech aesthetic, neon accents, no legible text.

The core follows this sequence:

  1. Preparation. prepare(text, font, options) takes the text and a font value in Canvas’s accepted format (for example, 16px Inter). It segments the content, measures the spans once, and returns an opaque handle. Options: { whiteSpace: 'pre-wrap' } to preserve spaces, tabs, and hard breaks; { wordBreak: 'keep-all' } for word-break: keep-all (CJK/hangul); { letterSpacing: n } in CSS pixels.
  2. Layout. layout(prepared, maxWidth, lineHeight) resolves how many lines fit and returns { height, lineCount }. It’s the step designed to repeat when the width changes.

High-tech illustration of a sub-millisecond layout hot path: a prepared text handle sits on the left, a bright neon beam enters a layout engine, and the output is a stack of glowing line boxes with abstract height and line-count indicators, multiple width sliders show the same text reflowing instantly across different container widths, cached width bars glow in the background, no DOM nodes, no canvas calls, and no string manipulation are visible, instead the scene emphasizes speed, repetition, and zero layout thrashing, ultra-detailed, 8K resolution, dark mode, cyberpunk/tech aesthetic, neon accents, no legible text.

  1. Manual control, if needed. prepareWithSegments() and layoutWithLines() return the lines; walkLineRanges(), measureLineStats(), layoutNextLineRange(), and materializeLineRange() let you walk them row by row without materializing the whole text. That enables drawing to Canvas, SVG, or around an obstacle whose width changes per row (the README includes an example that flows text around a floating image).

Dark futuristic scene showing manual line-range control for text rendering: a paragraph of abstract glyph lines flows around a floating holographic image obstacle, each line is a glowing box that can be walked, measured, and materialized individually, side panels display line ranges, per-line stats, and next-line calculations, the composition suggests Canvas, SVG, WebGL, and custom rendering use cases, with precise typography geometry and no full-page DOM dependency, ultra-detailed, 8K resolution, dark mode, cyberpunk/tech aesthetic, neon accents, no legible text.

The design relies on browser APIs rather than proprietary typography tables: Canvas provides segment widths, and Intl.Segmenter helps split words and graphemes for languages, emoji, and scripts without spaces. This matters because cutting a string by UTF-16 indices isn’t the same as cutting visible characters.

Official and semi-official status

The official surface is the README (the public source of truth for examples and limits), the development documentation (DEVELOPMENT.md), the platform bug registry (PLATFORM_BUGS.md), the CHANGELOG.md, the demos at chenglou.me/pretext, and the @chenglou/pretext npm package. It’s not part of any marketplace or formal standard; no organization “backs” it in a certification sense.

Its semi-official status, in practice, consists of TanStack Virtual’s adoption: TanStack’s official site dedicates a documentation page (“Text Measurement with Pretext”) to its use as a height estimator for text-dominated rows in chats, streams, feeds, comments, changelogs, and notifications, while TanStack keeps ownership of scroll, visible range, and positioning. None of these signals constitute a vendor endorsement of the methodology or its results; they’re integrations and technical references, not a standard designation.

Cyberpunk visualization of text virtualization and streaming interfaces: a tall scrolling feed of chat-like rows, comments, notifications, and changelog entries rendered as glowing abstract text blocks, a virtual viewport highlights only the visible rows while offscreen rows fade into low-opacity wireframes, a neon scroll indicator tracks position, floating measurement chips predict row heights before rendering, the scene suggests smooth performance in long lists, message streams, feeds, and real-time UIs, ultra-detailed, 8K resolution, dark mode, cyberpunk/tech aesthetic, neon accents, no legible text.

The ecosystem

Pretext quickly became a reference platform for a wave of ports, adapters, and experiments.

Author’s repositories: chenglou/pretext (the main library), chenglou/text-layout (34 stars, Markbåge’s earlier project, preserved as a predecessor), chenglou/react-motion (21,914 stars, the animation library Lou originally created), and chenglou/freerange (594 stars, static @fit checks for TypeScript layout code).

Ports to other languages and platforms: tornikegomareli/swift-pretextkit (183 stars, a Swift port for Apple platforms, released three days after the original), wieslawsoltes/PretextSharp (27 stars, text prep and line layout for SkiaSharp), craigm26/pretext_dart (8 stars) and nathankim0/pretext_flutter (7 stars, Dart/Flutter ports), waleed-qamar/flutter_pretext (35 stars, another Flutter port), hyj1230/pretext-py (4 stars, a Python rewrite), mjshin82/UnityPretext (46 stars, a Unity adapter), and fifteen42/pretext-video (90 stars, turns the webcam into live typography by combining Pretext with MediaPipe).

Cyberpunk ecosystem map of a text layout library spreading across programming languages and platforms: a central glowing core connects to satellite nodes for Swift, C#, Dart, Flutter, Python, Unity, React Native, Vite, Yoga-based DOM-less layout, and webcam typography experiments, each node has a distinct neon hue and abstract platform icon, data streams pulse between them, the scene conveys rapid adoption, ports, adapters, and community expansion without showing readable labels, ultra-detailed, 8K resolution, dark mode, cyberpunk/tech aesthetic, neon accents, no legible text.

Framework and tooling adapters: JubaKitiashvili/expo-pretext (259 stars, predicts React Native text heights before render), Agent-Pattern-Labs/textura (154 stars, “Pretext x Yoga = Textura,” a DOM-less layout engine), BaselAshraf81/layout-sans (67 stars, a pure-TypeScript 2D flex/grid layout engine powered by Pretext), LucasBassetti/react-pretext (2 stars, a headless React adapter), BALOTIAS/vite-pretext (4 stars, a zero-config Vite plugin to eliminate text-driven CLS), nikilok/virtual-text-layout (4 stars, a React hook that replaces TanStack Virtual’s measureElement), and lucascrespo23/pinch-type (112 stars, “pinch-to-zoom the text, not the page”).

Curated lists and neighboring projects: bluedusk/awesome-pretext (35 stars, a curated list of demos and tutorials), AsharibAli/pretext-skills (12 stars) and yaniv-golan/pretext-skill (15 stars), “skills” for AI agents. As a direct comparison, leeoniya published uWrap.js (mentioned in HN 43583478), a ~2 kb library for estimating ASCII row height, which they themselves contrasted against Pretext in the main thread.

The ecosystem’s overall pattern: a very small core that many extend to languages (Swift, C#, Dart, Python), to frameworks (React, React Native, Flutter, Vue, Vite, Unity), and to niche use cases (PDF, video, 3D), plus a layer of “skills” for AI agents.

Repo numbers

Measured: August 22, 2026, GitHub API and npm registry.

MetricValue
Stars49,966
Forks2,729
Real subscribers147
Commits (main branch)410
Open issues + PRs90
Primary languageTypeScript
LicenseMIT
CreatedMarch 7, 2026
Latest npm version0.0.8 (June 12, 2026)
npm downloads (week of Aug 14–20, 2026)905,501
npm downloads (month of Jul 22–Aug 20, 2026)4,127,107

watchers_count mirrors the star count, so subscribers_count is reported as the real subscriber figure. open_issues_count includes open change requests. The releases API returned an empty list: Pretext doesn’t publish formal GitHub releases, but does have git tags (v0.0.4…v0.0.8) and npm versions with a per-version CHANGELOG.md; the reference version is the npm one (0.0.8).

The top contributors by contribution count are chenglou (403), bg-l2norm (2), and, with one contribution each, brandonmcconnell, cxa, threepointone, bytaesu, and somnai-dreams. The concentration of 403 of 410 contributions in a single author confirms this is a single-maintainer project.

Quick-start guide

Installation and first run

npm install @chenglou/pretext

Requirements: a browser environment (or runtime) with Intl.Segmenter and Canvas 2D available. Pure Node without those APIs isn’t currently supported; server-side rendering is listed as future work. It’s ESM-only; there’s no direct CommonJS require() support.

First run in the browser:

import { prepare, layout } from '@chenglou/pretext'

const prepared = prepare('AGI 春天到了. بدأت الرحلة 🚀', '16px Inter')
const { height, lineCount } = layout(prepared, 320, 20)
// height and lineCount: pure arithmetic, no DOM layout or reflow

Common workflows

  1. Predicting a paragraph’s height before rendering. prepare(text, '16px Inter') once; layout(prepared, width, 20) as many times as the width changes. The result gives { height, lineCount } for anchoring scroll, sizing a virtualized row, or validating that a label fits inside a button.
  2. Flowing text around an image (editorial layout). With prepareWithSegments plus a LayoutCursor, call layoutNextLineRange() per row with a width that shrinks while the row is next to the image, and materializeLineRange() to get that row’s text.
  3. Multiline shrinkwrap (the minimum width that contains the text). measureLineStats(prepared, width) returns { lineCount, maxLineWidth }; walkLineRanges() walks the lines without building strings — useful for a binary search over a “pretty” width.
  4. Chat virtualization with TanStack Virtual. Cache prepare() per text and style, run layout() for the current width, and, when width/font/line-height change, reset Virtual’s measurements to recompute offsets.

Essential configuration

The 4–5 “settings” a new user will hit first are prepare()’s options and layout()’s parameters: font (a Canvas font string, e.g. 16px Inter, which must match the effective CSS of the measured text — use a named font, not system-ui), lineHeight (a layout() parameter that must match the CSS line-height), { whiteSpace: 'pre-wrap' } (for textarea-like text), { wordBreak: 'keep-all' } (for CJK/hangul), and { letterSpacing: n } (a CSS pixel value).

There are also two global helpers: setLocale(locale?) to set the word segmenter’s locale (and clear caches), and clearCache() to release the shared metrics cache when the app cycles through many fonts.

Common pitfalls and fixes

  • system-ui is unsafe on macOS. Canvas and DOM can resolve different fonts. Fix: use a named font. PLATFORM_BUGS.md links Chromium bug #489579956 and Mozilla bug #2020917.
  • Apple Color Emoji measures larger in Canvas than in DOM at small sizes (macOS, Chrome, and Firefox). The correction is detected by capability once per font and subtracted per emoji grapheme; disabling it drops the accuracy sweep from 7680/7680 to ~7652–7660/7680.
  • Wait for the font to load before preparing. If the font isn’t available, Canvas measurements will differ from what the browser applies later.
  • layout() with an empty string returns { lineCount: 0, height: 0 }. If different behavior is needed, clamp with Math.max(1, lineCount) * lineHeight.
  • Don’t call prepare() again without changes. It destroys the main advantage of precomputation; on a resize only layout() should re-run.
  • Safari needs a 1/64 px line-break tolerance (Chromium/Gecko use 0.005 px). And Safari 26.4 still fails WPT’s word-break-keep-all-006; the breakKeepAllAfterPunctuation fix stays in place until WebKit fixes it upstream.
  • Headless with devicePixelRatio = 1 masks the emoji and macOS system-ui bugs. Re-test in a headed browser on a Retina display.
  • Automatic hyphenation isn’t built in. Insert soft hyphens before prepare().
  • CSS outside the canvas.font shortcut (font-optical-sizing, font-feature-settings, font-variation-settings) isn’t modeled separately.

Integrations and migration

  • TanStack Virtual: officially documented integration as a height estimator for text-dominated rows. TanStack keeps ownership of scroll, range, and positioning; Pretext only supplies the estimate.
  • Migrating from DOM measurements: the migration pattern is replacing getBoundingClientRect()/offsetHeight with prepare()+layout() on the hot path, while keeping the real text in the DOM for accessibility.
  • Migrating from uWrap.js (leeoniya): uWrap is ~2 kb, ASCII/Latin only, white-space: pre-line only, with no soft hyphens or emoji; Pretext covers normal and pre-wrap, soft hyphens, emoji, and full multiline support. It’s a scope change, not an API change.
  • Integration with MCP/CI/editors: a dev-time use (verifying a label fits without a browser) and CI cases (geometry as a unit test), SSR (precomputed heights on hydration), and 3D/WebGL (line positions as coordinates). No official Pretext MCP server is documented.

Contributing

The development process is documented in DEVELOPMENT.md and AGENTS.md: one-time install with bun install (the project uses Bun, not npm, for development); daily development with bun start (serves the local demo site at http://localhost:3000); verification with bun run check (type-checking with tsc, linting with oxlint, dead-code scanning with knip) and bun test; Pretext-specific accuracy and benchmarking (bun run accuracy-check, accuracy-snapshot, benchmark-check, corpus-check/corpus-sweep); packaging with bun run build:package.

Documented rules: keep the README as the public source of truth; no monkey-patching (fix the root cause, not the symptom); the changelog carries only user-facing notes; diffs that change the hot engine must regenerate the accuracy and benchmark snapshots.

How the community received it

The recovered evidence shows strong enthusiasm, but also concrete criticism about performance and what people are paying attention to.

The main Hacker News thread (397 points, 69 comments, posted March 28, 2026) gathered notable opinions. simonw: “This thing is very impressive. The problem it solves is efficiently calculating the height of wrapped text on a web page, without rendering that text first (very expensive). It does this by precomputing the width/height of individual segments — think words — and caching…” rattray: “Regardless of the subject matter, the tweets announcing this are a masterclass in why an architectural/platform improvement can be high-impact.”

leeoniya (uWrap.js’s author) produced the most technical exchange: “I wrote something similar for this purpose, but much simpler and 2 kb, no AI, a year ago: uWrap.js… For ASCII text, mine finishes in 80ms, while Pretext takes 2200ms.” simonw replied that uWrap only handles Latin and no soft hyphens/emoji, and only pre-line. liuliu (a project collaborator) countered that prepare uses measureText in a loop and that “this library is meant to do prepare once and layout many times. layout calls should be sub-1ms.” leeoniya tried concatenating 100k sentences with line breaks: “wasn’t much faster, ~1880ms.”

lewisjoe: “Quick summary of pretext: if you want to lay out text on the web, you have to use the canvas.measureText API and implement wrapping/segmentation/RTL yourself. Pretext makes this easier.” rikroots: “Text layout engines are absurdly hard. You start thinking ‘it’s a hard task, but I can’ and three months later you find yourself in a corner screaming ‘why, Chinese?’” tadfisher (from Mozilla): “I’d love for exposing the browser’s text layout measurer to become a web API someday.”

In a secondary thread (16 points), sublinear objected: “As cool as this is, I find it hard to see this in a production web page. If there’s a JS dependency for styling, the dev still has to write fallback CSS.” In another thread (14 points, 5 comments), lewisjoe corrected an article about Pretext: “Pretext doesn’t free browser text layout entirely (at least not yet). It still uses canvas.measureText, which does the critical heavy lifting”; the article’s author, cyrusradfar, agreed and updated the post.

Simon Willison’s blog (March 29, 2026) presents it as “an exciting new browser library,” highlighting the The Great Gatsby verification method and the multilingual corpora. Den Odell’s blog (March 30, 2026, Dev.to) argues that “the community has spent three days building dragons [canvas demos]. It should be building chat interfaces” — the real feature, in his view, is predicting height without reading from the DOM.

Honest summary: enthusiasm is broad and high-level (397 points on the main thread). The real criticism comes from leeoniya (prepare performance in a loop) and sublinear (difficulty of production adoption due to the JS dependency for styling). cyrusradfar himself notes the SSR limitation still depends on Canvas.

Pretext versus other proposals

ProposalVerifiable overlapVerifiable difference
leeoniya / uWrap.jsEstimates row height for virtualization using Canvas’s measureText.uWrap is ~2 kb, Latin/ASCII only, white-space: pre-line only, no soft hyphens or emoji; Pretext covers normal and pre-wrap, soft hyphens, emoji, and full multiline support.
Skia-wasm (mentioned on HN)Renders text without depending on the browser DOM.Skia is a full rendering engine that “brings the world”; Pretext only measures and lays out, delegating the final render to the browser.
DOM measurements (getBoundingClientRect, offsetHeight)Return real, definitive geometry.Force synchronized reflow; in large lists they produce layout thrashing. Pretext avoids that read on the hot path.
CSS text-wrap: balance/pretty (mentioned on HN)Adjusts the number of words per line.Doesn’t eliminate empty space on the right edge or give height before render; it’s a complement, not a substitute.

The most useful comparison isn’t about popularity: Pretext stands out when repeated measurement sits on the critical path or when the app needs to explicitly own line breaks. For an ordinary web page with stable content, CSS, ResizeObserver, and semantic structure remain simpler and more appropriate.

Use cases and who this repository can help

  • Teams virtualizing text-dense lists (chats, feeds, comments, notifications, timelines) can use prepare()+layout() with TanStack Virtual to predict each row’s height and anchor scroll without visible corrections.
  • Builders of rich-text editors or streaming AI chat can recompute heights as tokens arrive without a shifting bubble continuously correcting the scroll position.
  • Canvas, SVG, WebGL, or 3D-engine developers can obtain lines and ranges to draw text in a DOM-less renderer, or compute line positions as coordinates and texture size before allocating GPU memory.
  • SSR/hydration teams can precompute heights during server rendering to hydrate the client with correct dimensions from the first frame and avoid CLS.
  • Anyone doing dev-time interface validation can verify that a label fits inside a button or that a 50-character string fits on a card without opening the browser.
  • Cross-platform teams can start from the core and use verified ports: Swift, React Native, Flutter, C#/SkiaSharp, or Python.
  • For an ordinary web page with stable content, CSS + ResizeObserver + semantic structure remain simpler and more appropriate.

Resources


Note: this article combines the README, DEVELOPMENT.md, PLATFORM_BUGS.md, the CHANGELOG.md, the @chenglou/pretext npm registry, the GitHub API, and articles by Simon Willison, Den Odell, and Cyrus Radfar, all consulted on August 22, 2026. Star, download, and contribution figures change over time.

Comments