Knuth-Plass Justified Text on the Web
Achieving professional-grade justified text in the browser using Knuth-Plass line breaking via tex-linebreak, canvas-based measurement via Pretext, and CSS typographic levers.
TL;DR
- Knuth-Plass line breaking produces ~3× more even word spacing than the browser’s built-in justification (σ≈4.2% vs σ≈11.8%), a significant and visible improvement across all column widths.
- CSS glyph scaling and letter spacing, techniques borrowed from professional print typesetting, give the algorithm much more room to distribute adjustments invisibly, keeping spacing consistent regardless of column width.
- These adjustments currently require measuring the rendered output, but newer canvas APIs may be able to predict the result without touching the DOM, potentially 71–91% faster at scale. Variable font width is not yet predictable this way.
- Canvas-based measurement (via Pretext) avoids DOM reflow entirely, making the full justification pipeline fast enough for real-time responsive layout. On the benchmark text it’s ~4.5× faster than DOM measurement, and the gap grows with DOM depth (see Section 6).
- With these techniques combined, web-based justified text can rival the quality of professional typesetting systems like TeX and InDesign.
1. The Problem
Browser-native CSS text-align: justify uses a greedy,
line-by-line algorithm: it fills each line independently without
considering how that choice affects subsequent lines. This produces
visibly uneven word spacing, especially at narrow column widths where
the greedy approach has few good options.
The Knuth-Plass algorithm (from “Breaking Paragraphs into Lines,” Knuth & Plass, 1981) solves this via global optimization: it evaluates all possible sets of line breaks simultaneously and picks the combination that minimizes total “demerits,” a penalty function of spacing deviation. The result is dramatically more even word spacing.
The reason this hasn’t happened on the web is cost. Knuth-Plass requires measuring every word in a paragraph before line-breaking can begin, and on the web, the standard way to measure text is through the DOM, which triggers synchronous layout reflow. That cost, combined with running the optimization itself in JavaScript, has kept browser justification stuck with the greedy algorithm.
2. Architecture
This project builds on two existing open-source libraries, tex-linebreak for line-breaking and Pretext for measurement, and adds a CSS rendering layer that translates optimal breakpoints into professionally justified HTML. The three components form a pipeline:
-
tex-linebreak
(
@robertknight/tex-linebreak)—a faithful JavaScript implementation of the Knuth-Plass algorithm by Robert Knight. It models text as a sequence of box (word), glue (space), and penalty (hyphenation) items, each with pre-measured widths and stretch/shrink tolerances. Given these items and a target line width, it evaluates all possible sets of breakpoints and returns the combination that minimizes total demerits. -
Pretext
(
@chenglou/pretext)—a canvas-based text measurement library that solves the cost problem described above. Instead of inserting text into the DOM and triggering layout reflow, Pretext segments text viaIntl.Segmenterand measures each segment viacanvas.measureText(), caching the results. This avoids synchronous reflow entirely, producing widths that agree with the DOM to within ~0.2px (~0.04% discrepancy), accurate enough for pixel-perfect justification. -
CSS rendering—the rendering layer that takes
Knuth-Plass breakpoints and produces justified HTML. Each line becomes
a
<span>with a computedword-spacingvalue. Optionally, the renderer applies two additional CSS adjustments, glyph scaling (viafont-variation-settings: "wdth"ortransform: scaleX()) and per-lineletter-spacing, to further improve spacing evenness. The integration of these CSS typographic levers into the Knuth-Plass optimization is this project’s primary contribution.
3. CSS Typographic Levers
Professional typesetting systems like Adobe InDesign and TeX’s microtype package use multiple levers beyond word spacing to achieve even justification. Word spacing is already integral to the Knuth-Plass algorithm: it’s the “glue” between words whose stretch and shrink capacity the optimizer adjusts. This project’s contribution is adding two more levers, glyph scaling and letter spacing, as additional stretch/shrink capacity fed into the optimizer, following industry best practices for acceptable value ranges.
The three levers:
- Word spacing—the primary lever, built into tex-linebreak’s glue model. The professional consensus on acceptable variation is roughly −0.05em to +0.08em around the font’s natural space width (Bringhurst, Adobe InDesign defaults, and the Pangram Pangram foundry recipe all converge on this range). This project uses the Pangram Pangram recipe: 85–130% of natural space width.
-
Glyph scaling—horizontal scaling of glyphs at
±2%. Bringhurst recommends ≤2% glyph scaling as the
permissible limit for justification, and Adobe, pdfTeX/microtype, and
most practitioners converge on the same ±2–3% range. At
this scale, the adjustment is invisible to readers but provides
significant flex for the optimizer. This project supports two CSS
implementations: the clean approach uses a variable font’s
wdthaxis (font-variation-settings: "wdth"), which scales at the outline level and preserves stroke weight; the fallback usestransform: scaleX(), which works with any font but distorts stroke weight. The variable font approach was inspired by Bram Stein’s work on linebreaking with variable fonts and finaltype.de’s “Better Justification for the Web”. -
Letter spacing—
letter-spacingat ±0.0075em. Bringhurst recommends ≤3% as the permissible limit for justification use. The modern professional consensus (Pangram Pangram, Speakipedia) settles on ±0.005–0.0075em per line, which this project adopts.
Without glyph scaling and letter spacing, the optimizer’s only flexibility is word spacing, and when word spacing alone cannot achieve a good adjustment ratio, the result is lines with visibly uneven gaps. With all three levers enabled, each word gains additional stretch/shrink capacity, allowing the optimizer to distribute adjustments across many small, imperceptible changes rather than concentrating them in word gaps. The quality comparison in Section 5 quantifies this: spacing evenness improves from σ≈11.8% (CSS greedy) to σ≈4.2% (KP + levers), roughly 3× more even word spacing across lines.
DOM Measurement as Correction
DOM correction (measuring the rendered line width via
getBoundingClientRect() and distributing the exact
remainder into word-spacing) is needed for
CSS adjustments because they are applied at render
time. These CSS properties change the rendered width from what canvas predicted, so a
measure-and-fill pass is required to achieve a flush right edge. Without
CSS adjustments, Pretext’s canvas widths are accurate enough for
pixel-perfect justification on their own.
To minimize the cost of this correction, the implementation batches all
DOM interaction into three phases: first, it creates all line elements
and applies CSS (transforms, font-variation-settings, letter-spacing)
without measuring anything; then it batch-reads all line widths via
getBoundingClientRect() in a single pass, triggering one
reflow; finally, it computes and applies per-line
word-spacing from the measured remainders. This
read-then-write batching avoids layout thrashing: what would otherwise
be N separate reflows (one per line) becomes a single reflow for the
entire paragraph.
4. Performance Benchmark
Setup
- Dataset: “The Crystal Goblet” by Beatrice Warde, 9 paragraphs, ~2000 words
- Scenario: Simulated responsive resize, re-justifying at 10 different widths (400–580px), 20 iterations each (200 total runs)
-
Font: Roboto Serif 18px with OpenType features
(
kern,liga,calt,onum) - Machine: macOS (Apple Silicon)
Three Approaches Tested
- Approach A: Pretext (canvas) + Knuth-Plass. Base justification with word-spacing only, no CSS adjustments, no DOM correction.
- Approach B: Pretext (canvas) + Knuth-Plass + glyph scaling (±2%) + letter spacing (±0.0075em) + DOM correction.
-
Approach C: DOM measurement (Range API) + Knuth-Plass
+ direct render. No correction needed since measurement and rendering
share the same coordinate system (though this relies on the renderer
using per-line
nowrapdivs; other renderers may still need correction).
Performance Results
| Phase | A: Pretext + KP | B: Pretext + KP + CSS | C: DOM + KP |
|---|---|---|---|
| Measurement | 0.92 ms (canvas) | 0.87 ms (canvas) | 4.12 ms (DOM) |
| Knuth-Plass | 1.37 ms | 1.27 ms | 0.92 ms |
| Render | 0.16 ms | 2.35 ms (+ DOM corr.) | 0.16 ms |
| Total per re-justify | 2.45 ms | 4.49 ms | 5.21 ms |
Performance Budget Context
| Budget | Threshold | A fits? | B fits? | C fits? |
|---|---|---|---|---|
| 60fps frame (16.7ms) | ~10ms for JS | Yes | Yes | Yes |
| Debounced resize (150ms) | ~100ms | Yes | Yes | Yes |
| Perceived instant (100ms) | ~80ms | Yes | Yes | Yes |
Key Findings
Pretext provides two distinct benefits:
- Speed: Canvas measurement is ~4.5× faster than DOM measurement on the benchmark text (~2,000 words, flat DOM). The gap is primarily DOM’s synchronous layout cost, so it grows with DOM depth and complexity (see Section 6). This makes Approach A (Pretext + KP, no CSS adjustments) the fastest path at 2.45ms, roughly half the time of the next fastest approach.
- Accuracy: Pretext’s canvas widths are accurate enough that base justification (word-spacing only) produces a flush right edge without any DOM correction.
For the highest-quality path (Approach B with CSS levers), Pretext still provides the fast initial measurement, but a DOM correction pass is needed to account for CSS-applied adjustments. The correction is not fixing Pretext’s inaccuracy; it’s accounting for the CSS properties that change widths at render time. Section 6 explores whether newer canvas APIs can eliminate this correction pass entirely.
5. Quality Comparison
Spacing Evenness (σ)
The most perceptually honest quality metric is σ, the standard deviation of word-spacing ratios across all lines in a paragraph. Lower σ means more even line-to-line spacing; if every line had the same spacing, σ would be 0.
| Approach | Typical σ | Interpretation |
|---|---|---|
CSS text-align: justify |
~11.8% | Noticeable line-to-line spacing variation |
| A: Pretext + KP (word-spacing only) | ~6–8% | Visibly more even |
| B: Pretext + KP + CSS levers | ~4.2% | ~3× more even than CSS, consistently tight spacing |
KP with CSS levers produces roughly 3× more even word spacing than the browser’s greedy algorithm. This is a significant, visible improvement. Paragraphs look noticeably more professionally set, without needing to overstate the magnitude.
Demerits (Technical Detail)
Demerits use the TeX formula (1 + badness)² where
badness = 100 × |r|³. Because the ratio is
cubed then squared, small spacing differences produce enormous demerit
ratios. This is useful for comparing optimizer configurations but not
proportional to perceptual difference. CSS demerits below are measured
from the browser’s actual rendered word-spacing via the Range API.
| Width | A: Pretext + KP | B: + GS/LS/DOM | CSS justify | A vs CSS | B vs CSS |
|---|---|---|---|---|---|
| 350px | ~569 | ~6 | ~63,342 | ~111× | ~10,083× |
| 400px | ~7,384 | ~7 | ~9,001 | ~1.2× | ~1,327× |
| 450px | ~347 | ~2 | ~2,460 | ~7× | ~1,268× |
| 500px | ~43 | ~2 | ~1,110 | ~26× | ~751× |
| 550px | ~155 | ~2 | ~482 | ~3× | ~301× |
| 600px | ~13 | ~1 | ~673 | ~50× | ~470× |
A/B vs CSS = ratio of CSS demerits to KP demerits (higher = KP is better). These large ratios reflect the cubic/squared formula, not proportional perceptual differences.
Key Findings
- KP + CSS levers produces ~3× more even spacing than CSS (σ≈4.2% vs σ≈11.8%). In demerits, this maps to 301–10,083× fewer demerits due to the cubic/squared formula, but the perceptual story is the σ number: consistently, visibly more even paragraphs.
- Approach A (KP with word-spacing only) is measurably better than CSS. σ drops to ~6–8%, still a clear improvement but with more variance across widths.
- CSS quality is highly width-sensitive. σ swings widely at narrow widths where the greedy algorithm has few good options. KP + CSS levers flattens this curve: σ stays stable regardless of width, because the levers give the optimizer enough slack to find consistently good solutions.
6. Canvas as a DOM Replacement
DOM correction triggers synchronous reflow, whose cost scales with DOM
complexity. Modern canvas APIs (ctx.letterSpacing,
ctx.wordSpacing, available in Chrome 99+ and Safari 17.5+)
can predict rendered line widths including CSS adjustments, potentially
replacing the reflow pass entirely. The
experiment page compares both pipelines
side by side.
At-Scale Benchmark
100 paragraphs rendered at three column widths and three levels of flexbox nesting. Knuth-Plass pre-computed; only the rendering phase was timed (3 runs averaged).
| Width | Nesting | DOM (ms) | Canvas (ms) | Speedup |
|---|---|---|---|---|
| 400px | 0 | 69.9 | 20.1 | 71% |
| 550px | 0 | 63.9 | 7.8 | 88% |
| 700px | 0 | 72.0 | 10.6 | 85% |
| 400px | 15 | 92.9 | 13.9 | 85% |
| 550px | 15 | 101.3 | 13.3 | 87% |
| 700px | 15 | 79.7 | 10.9 | 86% |
| 400px | 30 | 106.5 | 12.2 | 89% |
| 550px | 30 | 24.1 | 2.6 | 89% |
| 700px | 30 | 27.0 | 2.4 | 91% |
Canvas rendering is 71–91% faster across all configurations, and the advantage grows with DOM complexity. At 30 levels of flexbox nesting, DOM correction costs up to 106ms while canvas stays at ~12ms. Canvas time is nearly constant regardless of nesting because it never triggers reflow. The small-scale experiment (9 paragraphs) understated this gap because a shallow DOM makes batched reflow cheap.
A caveat: canvas APIs cannot yet predict width changes from variable
font wdth axis scaling, so full replacement of DOM
correction remains an open problem. Users who prioritize performance at
scale may consider transform: scaleX() for glyph scaling
instead, which canvas can handle entirely without DOM involvement,
though variable font wdth remains the highest typographic
quality option. For letter-spacing and
scaleX-based glyph scaling, the canvas results are
promising.
7. Conclusions
- Knuth-Plass produces significantly more even justification than CSS. With CSS typographic levers enabled, spacing evenness improves from σ≈11.8% to σ≈4.2%, roughly 3× more even word spacing across lines. (In the TeX demerit metric this maps to ~1,000× fewer demerits, but the cubic/squared formula amplifies small ratio differences, so the σ number is the more honest perceptual measure.) Even without levers, KP measurably improves spacing evenness over the browser’s greedy algorithm.
- CSS typographic levers dramatically improve quality. Glyph scaling (±2%) and letter spacing (±0.0075em) give the optimizer enough flex to find consistently excellent solutions across all column widths. Without them, KP still beats CSS, but quality varies more.
-
CSS levers currently require DOM correction, but canvas APIs are a
promising alternative.
Canvas
measureText()alone is accurate to ~0.2px, enough for pixel-perfect word-spacing-only justification. But glyph scaling and letter spacing change rendered widths in ways canvas cannot predict without help, requiring a DOM reflow pass. Modern canvas APIs (ctx.letterSpacing,ctx.wordSpacing) show promise for predicting these CSS-adjusted widths directly, which would eliminate the reflow bottleneck. A limitation: variable fontwdthaxis scaling is not yet predictable from canvas, so full replacement remains an open problem. - Pretext makes real-time justification practical. Because canvas measurement avoids synchronous DOM reflow, the full Pretext + KP pipeline completes in ~2.45ms, well within a single animation frame. This means justification can run on every responsive resize without perceptible delay, something DOM-based measurement makes difficult in complex pages. On the benchmark text (~2,000 words, flat DOM) canvas is ~4.5× faster than DOM, and the gap grows with DOM depth and complexity (Section 6 shows 71–91% faster at scale).
- The web can achieve professional-grade justification. By combining canvas measurement, Knuth-Plass optimization, professional typographic lever values, and DOM correction for CSS adjustments, web justification quality rivals dedicated typesetting systems—a result that was not achievable with CSS alone.