This page is a curated collection of working Pretext code examples — the patterns that come up repeatedly in real codebases, written so you can paste them in. Each example is independent and assumes you've installed @chenglou/pretext (npm install @chenglou/pretext).
If you searched for "pretext example" or "pretext examples", this is the page that turns the API documentation into running code. The examples are ordered roughly from "five-line hello world" to "multi-column magazine layout with image obstacles."
The minimum useful Pretext call:
import { prepare, layout } from '@chenglou/pretext';
const prepared = prepare(
"The quick brown fox jumps over the lazy dog.",
"16px Inter"
);
const { height, lineCount } = layout(prepared, 200);
console.log(`Height: ${height}px, Lines: ${lineCount}`);
// Height: 96px, Lines: 4 (depending on font metrics)
The two-line core: prepare() once per (text, font), layout() per width. Everything else builds on this.
A common React pattern: render a container at the correct height before commit, eliminating cumulative layout shift.
import { useMemo } from 'react';
import { prepare, layout } from '@chenglou/pretext';
function SizedText({ text, width }: { text: string; width: number }) {
const prepared = useMemo(() => prepare(text, "14px Inter"), [text]);
const { height } = useMemo(() => layout(prepared, width, 1.5), [prepared, width]);
return (
<div style={{ width, height, overflow: 'hidden', fontFamily: 'Inter', fontSize: 14 }}>
{text}
</div>
);
}
The container has its final height during render, not after. No skeleton flash, no CLS hit.
The fastest rendering path: skip the DOM entirely, draw to canvas with walkLineRanges.
import { prepare, walkLineRanges } from '@chenglou/pretext';
function drawText(
ctx: CanvasRenderingContext2D,
text: string,
x: number,
y: number,
maxWidth: number,
fontSize: number = 16,
font: string = 'Inter'
) {
const fontStr = `${fontSize}px ${font}`;
ctx.font = fontStr;
ctx.fillStyle = '#fff';
ctx.textBaseline = 'top';
const prepared = prepare(text, fontStr);
const lineHeight = fontSize * 1.4;
walkLineRanges(prepared, maxWidth, (start, end, i) => {
ctx.fillText(text.slice(start, end), x, y + i * lineHeight);
});
}
Zero allocations per line — walkLineRanges calls back with character indices, you slice the original string only when you actually draw. For a per-frame text animation, this is the path that doesn't garbage-collect.
The classic Pretext win — virtual scrolling without measurement guesses.
import { useMemo, useRef } from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { prepare, layout } from '@chenglou/pretext';
interface Message {
id: string;
text: string;
}
function MessageList({ messages, width }: { messages: Message[]; width: number }) {
const prepared = useMemo(
() => messages.map(m => prepare(m.text, "14px Inter")),
[messages]
);
const parentRef = useRef<HTMLDivElement>(null);
const v = useVirtualizer({
count: messages.length,
getScrollElement: () => parentRef.current,
estimateSize: (i) => layout(prepared[i], width, 1.5).height + 16, // padding
overscan: 8,
});
return (
<div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
<div style={{ height: v.getTotalSize(), position: 'relative' }}>
{v.getVirtualItems().map(item => (
<div
key={messages[item.index].id}
style={{
position: 'absolute',
top: item.start,
width,
padding: 8,
}}
>
{messages[item.index].text}
</div>
))}
</div>
</div>
);
}
estimateSize returns the exact height. The virtualizer never has to correct after measurement, and scroll position stays stable as items enter and exit the viewport.
Designers care whether a label fits. Test it with Pretext, before the browser tries.
import { prepare, layout } from '@chenglou/pretext';
function fitsInBox(
text: string,
font: string,
maxWidth: number,
maxHeight: number,
lineHeight: number = 1.4
): boolean {
const prepared = prepare(text, font);
const { height } = layout(prepared, maxWidth, lineHeight);
return height <= maxHeight;
}
// Usage in a unit test
test('product card title fits in 2 lines', () => {
expect(fitsInBox(longTitle, "16px Inter", 280, 16 * 1.4 * 2)).toBe(true);
});
The same function works in CI, in design tooling, and in build-time content validators. No headless browser required.
The "show 2 lines and an ellipsis" pattern, computed accurately.
import { prepare, layoutWithLines } from '@chenglou/pretext';
function truncateToLines(
text: string,
font: string,
maxWidth: number,
maxLines: number,
lineHeight: number = 1.4
): string {
const prepared = prepare(text, font);
const { lines } = layoutWithLines(prepared, maxWidth, lineHeight);
if (lines.length <= maxLines) return text;
const cutoff = lines[maxLines - 1].end;
return text.slice(0, cutoff).trimEnd() + '…';
}
Returns a truncated string ready to render. Unlike CSS line-clamp, this works in canvas, in SSR-rendered HTML, and in any context where you need the truncated string itself rather than a CSS hack.
For balanced layouts: "what's the narrowest width where this title fits in 2 lines?"
import { prepare, layout, type Prepared } from '@chenglou/pretext';
function widthForLines(
prepared: Prepared,
targetLines: number,
minWidth: number = 50,
maxWidth: number = 800
): number {
while (maxWidth - minWidth > 1) {
const mid = Math.floor((minWidth + maxWidth) / 2);
const { lineCount } = layout(prepared, mid);
if (lineCount > targetLines) minWidth = mid;
else maxWidth = mid;
}
return maxWidth;
}
const prepared = prepare("Engineering at Anthropic", "32px Inter");
const w = widthForLines(prepared, 2);
console.log(`Use width: ${w}px`);
layout() is so cheap that running 10–15 iterations of binary search is still microseconds total.
A magazine-style layout where the first paragraph wraps around an oversized capital.
import { prepare, layoutNextLine } from '@chenglou/pretext';
function layoutWithDropCap(
text: string,
font: string,
columnWidth: number,
dropCapWidth: number,
dropCapLines: number,
lineHeight: number
) {
const prepared = prepare(text, font);
const lines: { text: string; x: number; y: number }[] = [];
let cursor = 0;
let lineIndex = 0;
while (cursor < text.length) {
// First N lines have dropCapWidth removed from the left
const isWrapped = lineIndex < dropCapLines;
const x = isWrapped ? dropCapWidth + 12 : 0;
const w = isWrapped ? columnWidth - dropCapWidth - 12 : columnWidth;
const { end } = layoutNextLine(prepared, cursor, w);
if (end === cursor) break; // overflow safety
lines.push({ text: text.slice(cursor, end), x, y: lineIndex * lineHeight });
cursor = end;
lineIndex++;
}
return lines;
}
Each line is laid out at the width available at its vertical position. The drop cap reserves space on the first few lines; subsequent lines get the full column width.
Drag a sprite across paragraphs and watch the text reflow in real time, at 60fps.
import { prepare, walkLineRanges } from '@chenglou/pretext';
const text = "Lorem ipsum dolor sit amet… (long paragraph)";
const fontStr = "16px Inter";
const prepared = prepare(text, fontStr);
const ctx = canvas.getContext('2d')!;
ctx.font = fontStr;
let spriteX = 0, spriteY = 0;
const spriteRadius = 60;
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw sprite
ctx.fillStyle = '#34d399';
ctx.beginPath();
ctx.arc(spriteX, spriteY, spriteRadius, 0, Math.PI * 2);
ctx.fill();
// Re-layout text avoiding sprite, line-by-line
const lineHeight = 24;
let y = 0;
// (simplified — production uses layoutNextLine with per-line width based on sprite intersection)
walkLineRanges(prepared, canvas.width, (start, end, i) => {
ctx.fillStyle = '#eee';
ctx.fillText(text.slice(start, end), 0, i * lineHeight);
});
requestAnimationFrame(draw);
}
draw();
The full obstacle-aware version uses layoutNextLine per-line with a width that depends on whether the sprite intersects that horizontal slice. See the Drag Sprite Reflow demo for a working version.
For static content — blog posts, marketing pages — compute layouts at build time and ship a JSON sidecar.
// build script
import { prepare, layout } from '@chenglou/pretext';
import { writeFileSync } from 'fs';
const posts = await loadAllPosts();
const heights: Record<string, number> = {};
for (const post of posts) {
const prepared = prepare(post.body, "18px Georgia");
// Compute for our standard column width
heights[post.slug] = layout(prepared, 720, 1.6).height;
}
writeFileSync('content-heights.json', JSON.stringify(heights));
Note: this requires a Node-side canvas implementation like @napi-rs/canvas, since base Node has no font engine. With it, you ship correct dimensions in your initial HTML, eliminating CLS at the source.
Pretext handles Chinese, Japanese, and Korean correctly. The break opportunities are character-level.
import { prepare, layout, setLocale } from '@chenglou/pretext';
setLocale('ja-JP');
const prepared = prepare(
"これは日本語のテキストです。Pretext は CJK の改行ルールを正しく扱います。",
"16px 'Hiragino Sans', sans-serif"
);
const { height, lineCount } = layout(prepared, 280, 1.7);
The break behavior follows the locale — Japanese punctuation rules, line-end character restrictions, the kinsoku rule for characters that can't end or start a line. Set the locale explicitly per-app.
For textareas or code blocks that preserve whitespace:
import { prepare, layout } from '@chenglou/pretext';
const code = `function add(a, b) {
return a + b;
}`;
const prepared = prepare(code, "14px 'JetBrains Mono', monospace", {
whiteSpace: 'pre-wrap'
});
const { height } = layout(prepared, 600, 1.5);
pre-wrap preserves consecutive whitespace and treats newlines as hard breaks — the textarea/code-block default. Without it, multiple spaces collapse and newlines are treated as paragraph separators.
The 12 patterns above cover most production use cases. For deeper dives:
The library is at github.com/chenglou/pretext. The package is @chenglou/pretext on npm.
pretext.cool is a community-maintained showcase, not affiliated with Cheng Lou or the official Pretext project.