Most developers discover Pretext through a single problem: "How tall will this text be?" They call prepare(), call layout(), get a number, and move on. But Pretext is not a measurement utility. It is a framework for building text-driven applications — a structured pipeline that gives you full control over how text is segmented, measured, wrapped, and rendered.
This post explains the framework-level concepts that make Pretext different from a measurement function, and shows how to use them to build things that a simple measureText() call cannot support.
The Three-Stage Pipeline
Every Pretext workflow follows the same three stages: prepare, layout, render. This is not an implementation detail — it is the core architectural pattern that makes Pretext a framework rather than a function.
import { prepare, layout } from 'pretext';
// Stage 1: Prepare — segment text and measure glyphs
const prepared = prepare(text, {
font: '400 16px/1.5 "Inter", sans-serif',
whiteSpace: 'pre-wrap',
});
// Stage 2: Layout — compute line breaks and geometry
const result = layout(prepared, containerWidth);
// Stage 3: Render — use the result however you want
applyToDOM(result); // or canvas, or WebGL, or server templateEach stage has a distinct purpose and a distinct cost profile:
Prepare is the expensive step. It reads font metrics, segments text into words and grapheme clusters, and builds a measurement table. You pay this cost once per text string.
Layout is the cheap step. It takes the prepared data and a width, then computes line breaks using pure arithmetic. No DOM, no canvas, no I/O. You can call layout() thousands of times per frame at different widths — it is essentially free.
Render is your responsibility. Pretext does not touch the DOM. It gives you the numbers; you decide what to do with them. This separation is what makes Pretext work in browsers, workers, servers, and native runtimes.

Why the Pipeline Matters
A single measureText() function conflates preparation, layout, and rendering into one opaque call. That means:
- You cannot re-layout at a different width without re-measuring glyphs
- You cannot inspect individual lines or line breaks
- You cannot share preparation work across multiple layout passes
- You cannot run layout off the main thread
Pretext's pipeline eliminates all four limitations. The prepared object is serializable. You can prepare text on the main thread, transfer the result to a worker, and run layout there. You can prepare once and layout at 50 different widths to find the optimal container size. You can cache the prepared object and re-layout when the viewport resizes — for free.
// Prepare once
const prepared = prepare(longArticle, { font: '16px/1.6 Georgia' });
// Layout at every breakpoint — near zero cost
const layouts = [320, 480, 768, 1024, 1280].map(w => ({
width: w,
...layout(prepared, w),
}));
// Find the breakpoint where line count changes
const breakpoints = layouts.filter((l, i) =>
i === 0 || l.lines !== layouts[i - 1].lines
);This pattern is impossible with DOM measurement. Each width would require a reflow. With Pretext, the five layout() calls together take less time than a single offsetHeight read.
Line-Level Iteration
Most measurement APIs give you a bounding box — width and height. Pretext gives you every line. The walkLineRanges() API iterates over line break positions, giving you the character ranges and geometry of each line.
This is what turns Pretext from a measurement tool into a layout framework. With line-level data, you can:
- Animate text line by line — stagger entrance animations per line
- Wrap text around shapes — vary the available width per line to flow text around circles, images, or irregular shapes
- Implement custom line breaking — apply Knuth-Plass optimal breaking or other algorithms using Pretext's glyph measurements
- Build text editors — map cursor positions to line/column coordinates without DOM queries
import { prepare, layoutNextLine } from 'pretext';
const prepared = prepare(text, { font: '16px/1.5 system-ui' });
// Layout line by line with varying widths
let offset = 0;
let y = 0;
const lines = [];
while (offset < text.length) {
// Width varies per line — text wraps around a circle
const lineWidth = getAvailableWidth(y, circleCenter, circleRadius);
const line = layoutNextLine(prepared, lineWidth, offset);
lines.push({ ...line, y });
offset = line.end;
y += line.height;
}The layoutNextLine() API is the low-level primitive. It computes a single line break, returning the character range and dimensions. You control the loop, the width, and the vertical position. This is the API that powers the editorial engine demo — text reflowing around animated obstacles at 60fps.
Environment Independence
Pretext runs anywhere JavaScript runs. This is not a convenience feature — it is a design constraint that shapes the entire framework.
Because Pretext never touches the DOM, it works in:
- Web Workers — offload layout computation from the main thread
- Node.js — server-side rendering with accurate text dimensions
- Edge functions — Cloudflare Workers, Deno Deploy, Vercel Edge
- React Native — text measurement without native bridges
- Build tools — static site generators that need text dimensions at compile time
The only browser API Pretext uses is canvas.measureText() during the prepare() step — and even that can be replaced with a custom measurement function for non-browser environments.
// Custom measurement for server environments
const prepared = prepare(text, {
font: '16px/1.5 sans-serif',
measureText: (text, font) => {
// Use node-canvas, Skia, or a font metrics library
return myServerMeasure(text, font);
},
});Composing with Other Libraries
Because Pretext outputs data — not DOM nodes — it composes naturally with any rendering layer:
With React
function AutoSizedBubble({ message, maxWidth }) {
const { width, height } = useMemo(() => {
const p = prepare(message, { font: '15px/1.4 system-ui' });
return layout(p, maxWidth);
}, [message, maxWidth]);
return (
<div style={{ width, minHeight: height, padding: '8px 12px' }}>
{message}
</div>
);
}With Canvas / WebGL
const prepared = prepare(text, { font: '24px monospace' });
const result = layout(prepared, canvas.width);
// Draw each line at its computed position
result.lineRanges.forEach(({ start, end, y }) => {
ctx.fillText(text.slice(start, end), 0, y);
});With Virtual Scroll Libraries
// Compute all heights upfront — no lazy measurement needed
const heights = items.map(item => {
const p = prepare(item.text, { font });
return layout(p, columnWidth).height + padding;
});
// Pass to any virtual scroll library
<VirtualList itemCount={items.length} itemSize={i => heights[i]} />The Framework Mindset
The difference between a library and a framework is inversion of control. A library is a function you call. A framework is a structure you build within.
Pretext inverts control over text layout. Instead of asking the browser "how tall is this text?" and accepting whatever it returns, you decompose the problem into preparation, layout, and rendering — and you control each step.
This decomposition is what enables the community showcases — projects that use Pretext for creative typography, game interfaces, 3D text wrapping, and interactive experiences that would be impossible with DOM measurement alone.
When you think of Pretext as prepare() + layout() = height, you are using 10% of it. When you think of it as a pipeline for decomposing text layout into measurable, composable, environment-independent steps, you start building things that were never possible before.
Getting Started
npm install pretextStart with the interactive examples to see what the framework can do. Read the docs for the full API reference. And if you build something interesting, share it — the community showcases are full of projects that started with "I wonder if Pretext can do this."
Learn more at pretextjs.dev or explore the interactive demos.