This page is the API reference for Pretext (@chenglou/pretext), the pure-JavaScript text layout library. It documents every public function — signature, behavior, parameters, return values, examples, and the gotchas that show up in production. If the official README is the marketing pitch, this is the spec sheet.
The API is intentionally small. Eight exported functions cover everything from a simple text height query to per-line iteration with variable widths. The split is two core functions you'll use 95% of the time, four advanced ones for richer cases, and two utility functions for cache and locale control.
import {
// Core
prepare,
layout,
layoutWithLines,
// Advanced
prepareWithSegments,
walkLineRanges,
layoutNextLine,
// Utility
clearCache,
setLocale,
} from '@chenglou/pretext';
The package is ESM-first with a CJS fallback. Tree-shaking works cleanly — importing only what you use means the bundle only includes what you imported.
The one-time measurement call. Tokenizes text, identifies break opportunities using Unicode segmentation rules, measures glyph widths via canvas measureText(), and returns an opaque "prepared" object that subsequent layout calls operate on.
Signature
function prepare(
text: string,
font: string,
options?: PrepareOptions
): Prepared;
interface PrepareOptions {
whiteSpace?: 'normal' | 'pre-wrap'; // default 'normal'
wordBreak?: 'normal' | 'break-all'; // default 'normal'
}
Parameters
text: the multiline (or single-line) text to measure. Can contain newlines, multiple consecutive spaces, tabs, emoji, and any Unicode script.font: a canvas font string. Format follows the CSS font shorthand: "[style] [weight] size font-family". Examples: "16px Inter", "600 18px 'Helvetica Neue'", "italic 14px serif".options.whiteSpace: 'normal' collapses consecutive whitespace and ignores newlines as paragraph breaks (the default for most web text). 'pre-wrap' preserves whitespace and treats newlines as hard breaks (the default for textareas and code blocks).options.wordBreak: 'normal' breaks on word boundaries; 'break-all' breaks at any character (useful for narrow columns containing long unbroken strings like URLs).Returns: an opaque Prepared object. Don't introspect its shape; pass it to the layout functions.
Example
const prepared = prepare(
"The quick brown fox jumps over the lazy dog.",
"16px Georgia"
);
Cost: ~0.04ms per short string on a modern laptop, or roughly 19ms for 500 distinct strings. Call once per (text, font) combination and cache.
Gotchas
"16px, Georgia" (with a comma) will throw."16px 'Helvetica Neue'".await document.fonts.ready before calling prepare() for the first time after page load.The hot-path function. Pure arithmetic over cached data. Returns the height and line count for the prepared text laid out at a given width.
Signature
function layout(
prepared: Prepared,
maxWidth: number,
lineHeight?: number // default 1.2
): LayoutResult;
interface LayoutResult {
height: number; // in CSS pixels
lineCount: number;
}
Parameters
prepared: the result of prepare().maxWidth: the available width in CSS pixels. The layout will break lines as needed to fit within this width.lineHeight: a unitless multiplier on the font size (matching CSS unitless line-height). Default 1.2; pass 1.4 or 1.5 for more comfortable reading.Returns: { height, lineCount }.
Example
const { height, lineCount } = layout(prepared, 320, 1.5);
// height: 144 lineCount: 6
Cost: microseconds. Safe to call inside requestAnimationFrame for every visible row of a long list.
Gotchas
maxWidth is in CSS pixels, not device pixels. No retina conversion needed.maxWidth is smaller than the widest unbreakable token in the text, the layout will overflow that token. Set wordBreak: 'break-all' in prepare() to avoid.Same inputs as layout(), but returns per-line geometry.
Signature
function layoutWithLines(
prepared: Prepared,
maxWidth: number,
lineHeight?: number
): LayoutWithLinesResult;
interface LineInfo {
text: string; // the substring on this line
width: number; // actual rendered width in CSS pixels
start: number; // character index in original text (inclusive)
end: number; // character index in original text (exclusive)
}
interface LayoutWithLinesResult extends LayoutResult {
lines: LineInfo[];
}
Example
const result = layoutWithLines(prepared, 200, 1.4);
result.lines.forEach((line, i) => {
console.log(`Line ${i}: "${line.text}" (${line.width}px)`);
});
When to use: rendering text yourself (canvas, SVG, WebGL), highlighting individual lines, building per-line animations.
When not to use: when you only need the total height — layout() is faster because it doesn't allocate the lines array.
A richer variant of prepare() that exposes the segmentation data — the individual word, whitespace, break-opportunity, CJK, and emoji segments the engine identified.
Signature
function prepareWithSegments(
text: string,
font: string,
options?: PrepareOptions
): PreparedWithSegments;
interface Segment {
start: number;
end: number;
width: number;
kind: 'word' | 'whitespace' | 'break-opportunity' | 'cjk' | 'emoji';
}
type PreparedWithSegments = Prepared & {
readonly segments: readonly Segment[];
};
When to use: tooling that needs to introspect the engine's decisions, syntax highlighters that need per-word geometry, internationalization debugging.
When not to use: regular layout — the segments array is overhead you don't need for plain measurement.
Iterate over lines without allocating a lines array or substring text. The fastest way to render line-by-line.
Signature
function walkLineRanges(
prepared: Prepared,
maxWidth: number,
callback: (start: number, end: number, lineIndex: number) => void
): void;
Example
walkLineRanges(prepared, 320, (start, end, i) => {
ctx.fillText(text.slice(start, end), 0, i * lineHeight);
});
When to use: canvas renderers that draw substrings; high-frequency rendering (per-frame); zero-allocation hot paths.
Lay out a single line at a given starting position and width. The building block for variable-width layouts (text flowing around obstacles, multi-column layouts with varying column widths).
Signature
function layoutNextLine(
prepared: Prepared,
start: number, // character index to start from
maxWidth: number
): NextLineResult;
interface NextLineResult {
end: number; // character index where the line ended (exclusive)
width: number; // actual width in CSS pixels
}
Example
let cursor = 0;
let y = 0;
while (cursor < text.length) {
const width = availableWidthAt(y); // your function: returns column width at this y
const { end } = layoutNextLine(prepared, cursor, width);
if (end === cursor) break; // safety: avoid infinite loop on overflow
drawLine(text.slice(cursor, end), 0, y);
cursor = end;
y += lineHeight;
}
When to use: any layout where the available width changes line-to-line. Magazine layouts, drop-cap text wrap, image-flow text, table-of-contents columns with varying widths.
Gotcha: if maxWidth is too narrow to fit any token, layoutNextLine returns end === start. Always check for this to avoid infinite loops.
Clear the internal measurement cache. Call after a font swap or when you've prepared transient text you don't expect to re-render.
Signature
function clearCache(): void;
When to use: app-wide font theme change, document.fonts.onloadingdone handler, memory pressure.
When not to use: routinely. The cache is the source of Pretext's speed; clearing it forces every subsequent prepare() to re-measure from the canvas font engine.
Configure the engine's locale for line-break behavior. Some scripts (Japanese, Korean, Thai) have locale-sensitive rules.
Signature
function setLocale(locale?: string): void; // BCP 47 language tag, e.g. 'ja-JP'
Example
import { setLocale, prepare } from '@chenglou/pretext';
setLocale('ja-JP');
const prepared = prepare("日本語のテキスト", "16px 'Hiragino Sans'");
When to use: localized apps that switch language at runtime; debugging script-specific break behavior.
Gotcha: this is a global setting, not per-call. If you prepare text in multiple locales, set the locale, prepare, then either reset or accept the global state.
The full list of types Pretext exports:
export type {
Prepared,
PreparedWithSegments,
PrepareOptions,
LayoutResult,
LayoutWithLinesResult,
LineInfo,
NextLineResult,
Segment,
};
Prepared is intentionally opaque (branded). Don't depend on its internal shape.
A few small recipes that combine the API:
Pattern: Memoized text measurement
const cache = new Map<string, Prepared>();
function getPrepared(text: string, font: string): Prepared {
const key = `${font}\u0000${text}`;
let p = cache.get(key);
if (!p) {
p = prepare(text, font);
cache.set(key, p);
}
return p;
}
Pattern: Find optimal container width via binary search
function findWidthForLines(prepared: Prepared, targetLines: number, min = 50, max = 800): number {
while (max - min > 1) {
const mid = Math.floor((min + max) / 2);
const { lineCount } = layout(prepared, mid);
if (lineCount > targetLines) min = mid;
else max = mid;
}
return max;
}
Pattern: Detect text overflow
function overflowsAt(prepared: Prepared, maxWidth: number, maxHeight: number, lineHeight = 1.4): boolean {
return layout(prepared, maxWidth, lineHeight).height > maxHeight;
}
For the architectural deep dive on how prepare() and layout() actually work internally, see the How Pretext Works post. For framework-specific integration, see the Pretext + React guide or the Pretext + TypeScript guide. For copy-pasteable code patterns, see the Pretext Examples page.
The library lives at github.com/chenglou/pretext. The npm package is @chenglou/pretext.
pretext.cool is a community-maintained showcase, not affiliated with Cheng Lou or the official Pretext project.