CSS is great at laying out text. The browser's text engine is decades of engineering, optimized to within an inch of its life, used by literally billions of pages every day. So why would anyone reach for Pretext — a JavaScript text layout library — when CSS is right there?
This page answers that question by working through six concrete tasks where CSS hits a wall and Pretext walks through it. If you searched for "pretext css" trying to figure out whether the library replaces, complements, or competes with CSS, the short answer is: complements. Pretext doesn't render text — that's still CSS or canvas's job. Pretext measures text and tells you where it'll wrap, before the browser commits.
Before the comparison, the boundary: Pretext doesn't render anything. It doesn't know about colors, decorations, gradient text fills, or any of the visual properties that CSS handles. It's purely a measurement and break-position engine. The output of Pretext is { height, lineCount } or { lines: [{ start, end, width }] } — numbers and indices, not pixels.
So when you use Pretext, CSS is still doing the rendering. You're using JavaScript to compute where text will wrap and how tall it will be, then you set CSS properties (width, height) accordingly, and the browser renders.
With that framing, here are six places this division of labor pays off.
The CSS way: render the text into a hidden container, then read back getBoundingClientRect().height after layout.
function measureWithDOM(text: string, width: number, font: string): number {
const div = document.createElement('div');
div.style.cssText = `position:absolute;visibility:hidden;width:${width}px;font:${font}`;
div.textContent = text;
document.body.appendChild(div);
const h = div.getBoundingClientRect().height;
document.body.removeChild(div);
return h;
}
Cost: ~1ms per call on a modern laptop, dominated by DOM mutation and forced synchronous layout. If you call this in a loop (1000 messages in a virtual list), you're staring at a 1-second hitch.
The Pretext way:
import { prepare, layout } from '@chenglou/pretext';
function measureWithPretext(text: string, width: number, font: string): number {
const prepared = prepare(text, font); // ~0.04ms, cached
return layout(prepared, width).height; // microseconds
}
100x to 500x faster, no DOM mutation, no forced layout, no allocations on the layout call.
When CSS is fine: one-off measurement, not in a hot path. When Pretext wins: any loop, any hot path, any case where you measure 100+ strings.
The CSS way: -webkit-line-clamp with the WebKit-prefixed display: -webkit-box.
.clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
This works beautifully for browser rendering. But:
The Pretext way:
import { prepare, layoutWithLines } from '@chenglou/pretext';
function clampToLines(text: string, font: string, width: number, lines: number): string {
const prepared = prepare(text, font);
const result = layoutWithLines(prepared, width);
if (result.lines.length <= lines) return text;
const cutoff = result.lines[lines - 1].end;
return text.slice(0, cutoff).trimEnd() + '…';
}
You get the truncated string. It works in every rendering target. It tells you whether truncation happened (by checking result.lines.length).
When CSS is fine: visual clamping in pure browser rendering, no need for the truncated string. When Pretext wins: anywhere you need the truncated string itself, server-rendered HTML, canvas, search-result snippets, email previews.
The CSS way: don't set a height; let the text size the container. The container will be the right size — but only after layout. Before layout, the container has no height, which means anything below it shifts.
This is the canonical CLS (Cumulative Layout Shift) problem. Web Vitals will mark you down for it.
The CSS workaround: set a min-height based on a guess. If the guess is high, you get awkward whitespace below short text. If the guess is low, you still get layout shift for long text.
The Pretext way:
function SizedContainer({ text, width, font }: Props) {
const prepared = useMemo(() => prepare(text, font), [text, font]);
const { height } = useMemo(() => layout(prepared, width, 1.5), [prepared, width]);
return <div style={{ width, height }}>{text}</div>;
}
The container has its final height during render, before commit. No CLS, no skeleton, no min-height guess.
When CSS is fine: when CLS isn't a metric you optimize for, or when you're rendering text that doesn't change layout-altering properties. When Pretext wins: when you measure CLS, when text is dynamic (chat, comments, CMS-driven pages), when content shifts hurt user experience.
The CSS way: it doesn't, really. CSS doesn't know about virtual scrolling; it's a JavaScript pattern. The library you choose (react-virtuoso, @tanstack/react-virtual, virtua) needs a per-row height. Without Pretext, you have two bad options:
The Pretext way: pass layout() as the per-row height function.
const virtualizer = useVirtualizer({
count: messages.length,
estimateSize: (i) => layout(prepared[i], width).height,
// ...
});
estimateSize returns the actual height. The virtualizer never has to correct. Scrolling is smooth.
When CSS is fine: never, for variable-height virtual scrolling. CSS doesn't compete here. When Pretext wins: every variable-height virtual scroller benefits.
"Make this title as large as possible without overflowing this box."
The CSS way: there isn't a great one. The closest is font-size: clamp() with viewport units, but it's coarse — it doesn't actually fit text to a box, it just scales font size proportionally to viewport width. For real fit-to-box, people use libraries like textfit that binary-search the font size by repeatedly measuring with the DOM.
The Pretext way: binary-search the font size, but measure with Pretext.
import { prepare, layout } from '@chenglou/pretext';
function fitFontSize(text: string, family: string, maxW: number, maxH: number): number {
let lo = 8, hi = 200;
while (hi - lo > 0.5) {
const mid = (lo + hi) / 2;
const prepared = prepare(text, `${mid}px ${family}`);
const { height } = layout(prepared, maxW, 1.2);
if (height > maxH) hi = mid;
else lo = mid;
}
return lo;
}
15 iterations of binary search, each measuring in microseconds. Total cost: under a millisecond. The DOM-measurement equivalent would be 15ms, with 15 forced layouts.
When CSS is fine: never, for this task. When Pretext wins: dashboards with sized cards, social-media share images with fitted text, dynamic poster generation.
The CSS way: the server doesn't know how text will wrap, so it can't ship correct dimensions. The browser computes the layout after the HTML arrives. This is fine for most sites but creates the CLS problem above for content where the dimensions matter (image galleries, chat, infinite scroll).
The Pretext way: with @napi-rs/canvas installed, run Pretext at SSR time and ship dimensions in the HTML.
// server.tsx
import { prepare, layout } from '@chenglou/pretext';
const heights = posts.map(p => layout(prepare(p.text, '16px Inter'), 720).height);
// ship `heights` as inline data, set container heights at SSR time
The HTML arrives with correct heights. The browser uses them immediately. Zero CLS, no measurement pass needed on the client.
When CSS is fine: for static text where dimensions are known at design time. When Pretext wins: for dynamic content where dimensions depend on user input or runtime data.
To balance the comparison: there are tasks where reaching for Pretext is overkill.
The pattern: use Pretext when you need to know dimensions before rendering. Use CSS when you just need to render.
The most common production pattern:
width, height, and style properties on its container.You get the speed and predictability of pre-computation with the polish and accuracy of native browser rendering. Neither tool is doing the other's job.
function MeasuredText({ text, width }: Props) {
const { height } = useTextLayout(text, '14px Inter', width);
return (
<div
style={{ width, height, fontFamily: 'Inter', fontSize: 14, lineHeight: 1.4 }}
>
{text}
</div>
);
}
Pretext computes the height. CSS draws the text inside it. The browser never has to measure-and-resize.
To list the CSS features that Pretext meaningfully replaces:
getBoundingClientRect() for text width/height (Pretext is faster and predictable)-webkit-line-clamp when you need the truncated string (Pretext gives you the string)Everything else stays CSS.
If you want the React-specific patterns built on top of this hybrid model, see the Pretext + React guide. If you want the architectural background on why Pretext is so much faster than DOM measurement, read How Pretext Works. For the API itself, see the Pretext API reference.
The library lives at github.com/chenglou/pretext and the package is at @chenglou/pretext.
pretext.cool is a community-maintained showcase, not affiliated with Cheng Lou or the official Pretext project.