Skip to content
Jot
Home
Trending
Top
Tags
Groups
Theme
Privacy·Terms
Sign in
Home
Search
Trending
Sign in

Justified text on the web is a lie we agreed to

G
Gerard Aalders
·December 26, 2025·1,258 words·7 min read
designmarkdowntypographywriting
☆stars: 13⑂forks: 0

A compositor setting a page of type by hand had four moves on any given line. Widen the word spaces. Tighten them. Break a word at a syllable. Go back and reset the two lines above. A browser has one of those and it is the worst one.

So when you write text-align: justify you are not asking for justified text. You are asking for the line breaks you were already going to get, with the leftover space shovelled into the word gaps. You can tell which of those two things you are looking at from across a room.

How the box gets filled

The engine takes words until one will not fit, breaks, and forgets that line existed. Then it does it again. The people who build the engines are not coy about it. From the WebKit post announcing their new wrapping work:

This is often called a 'first-fit' or 'greedy' algorithm.

The forgetting is the problem. When the engine commits line four it has no idea that line five will be one short word, and it cannot go back and look. Justification then happens as arithmetic on whatever slack the breaking left behind. The breaking is the decision. The spacing is the invoice.

The same paragraph, two ways

Same words, 32 character measure. I ran these rather than drew them: greedy first fit, then the same text broken with the whole paragraph in view and a cost that punishes slack.

--- greedy, first fit --------- worst mean word gap 4.33 char
Justification  on a printed page
was     a    negotiation.    The
compositor  could  reset the two
lines  above,  break a word at a
syllable,  or  ask for a shorter
sentence. A browser does none of
that. It fills one line, forgets
it,  and  fills  the  next,  and
every  gap it leaves behind gets
paid for out of the word spaces.

--- whole paragraph in view --- worst mean word gap 3.00 char
Justification   on   a   printed
page   was  a  negotiation.  The
compositor  could  reset the two
lines  above,  break a word at a
syllable,  or  ask for a shorter
sentence. A browser does none of
that. It fills one line, forgets
it,  and  fills  the  next,  and
every  gap it leaves behind gets
paid for out of the word spaces.

Greedy takes printed page up onto line one, which strands four words and three enormous gaps on line two. The whole-paragraph version pushes page down, accepts a gappier first line, and the worst mean gap in the paragraph falls from 4.33 characters to 3.00. Lines three to ten are identical in both, which is normal. The two algorithms usually differ over two or three lines out of ten, and those are the lines you notice.

One cheat you should know about. Monospace cannot divide a space, so my gaps come out uneven within a line. A browser spreads the slack in fractions of a pixel, evenly. The stretch per line is identical; it is just paid more politely.

Rivers

A river is a run of word spaces on consecutive lines sitting close enough in the horizontal that the eye joins them into a channel. Get four or five to coincide and you have a pale blotch, which the trade calls a lake, and once you have seen one you cannot stop. Rivers are a property of the relationship between lines. Nothing in a first fit breaker looks at that, and nothing in CSS lets you ask.

Knuth and Plass settled the breaking problem in 1981 (Breaking paragraphs into lines, Software: Practice and Experience, volume 11, pages 1119 to 1184) by costing every possible set of break points and solving the paragraph in one pass of dynamic programming. That is what TeX does. Even TeX does not hunt rivers, it just makes them unlikely by refusing to leave big gaps anywhere. WebKit are honest about being in the same position:

We are not yet making adjustments to prevent rivers, although we'd love to in the future.

What has changed since I last complained about this

Three things, and I have been unfair by going on quoting the 2015 version of this argument.

hyphens: auto works now. Safari since 5.1, Firefox since 6, Chrome only since 88, which is 2021; before that you could set it and watch nothing happen. It needs a language, so declare lang on the root element and get it right, because English break points inside Dutch compounds are worse than no hyphenation at all. Here is the same greedy breaker given permission to hyphenate, which is the only result in this piece that surprised me:

--- greedy, plus hyphenation -- worst mean word gap 2.00 char
Justification  on a printed page
was  a negotiation. The composi-
tor  could  reset  the two lines
above,  break a word at a sylla-
ble,  or  ask for a shorter sen-
tence.  A  browser  does none of
that. It fills one line, forgets
it,  and  fills  the  next,  and
every  gap it leaves behind gets
paid for out of the word spaces.

4.33 down to 2.00, on the dumb algorithm, in the same ten lines. It beat the clever breaker by a distance. I supplied the syllable points by hand, so read that as the mechanism rather than as one browser's output, but the mechanism is the argument: the biggest lever on justified text is permission to break words, and it always was. A printed page you admire has hyphens down its right edge and you have never once counted them.

text-wrap: pretty exists. Chrome shipped it in 117 with an approximation of Knuth and Plass, but the scope is small:

Chromium only makes adjustments to the last four lines of a paragraph.

Safari 26 does the whole paragraph. Firefox does not support it at all. caniuse has it around 85 per cent of traffic, which is enough to switch on and not enough to build on. text-wrap: balance is for headings rather than body copy: Chromium gives up above six wrapped lines and Firefox above ten, and both are right to.

Do not put text-wrap: pretty and text-align: justify on the same element. WebKit describe what pretty does like this: the browser instead aims to wrap each line sooner than the maximum limit of the text box. Correct for a ragged edge. Justify then takes the line you deliberately made short and stretches it back to the full measure, so the shortfall you engineered comes out of the word spaces. You have asked for narrower lines and agreed to pay for them in the one currency you were trying to protect.

The controls you would need to finish the job are the ones nobody has. text-justify, which is how you would ask for space between characters instead of between words, is unsupported in Safari and only reached Chrome at 145. hanging-punctuation, which stops an opening quote cutting a notch in your left edge, is Safari only, about 16 per cent of traffic. The systems that justify well also stretch the glyph outlines themselves by a fraction of a per cent to absorb slack invisibly, and no browser does anything of the kind.

What I set

html { hyphens: auto }        /* and declare lang on the root, every time */

p {
  max-width: 32em;            /* measure your own face, see below */
  text-align: left;
  text-wrap: pretty;
}

The 32em is not a number to copy. Set the lowercase alphabet twice in your body face at your body size, measure it, divide by 52, multiply by 65. In the text faces I have measured that lands between 30 and 34em. Do not reach for 65ch. The ch unit is the advance of the zero, and the zero is drawn wide because it has to stand in a column with the other figures. In the face this platform sets its headings in, the zero is 460 units on a 1000 unit em while the unweighted mean of the twenty six lowercase advances is 392. That is 17 per cent, so 65ch quietly hands you about 76 characters and you are past Bringhurst's upper bound without having decided to be.1

@nullisland/every-projection-is-wrong-here-is-how-much makes the same argument about maps with better numbers than mine. You do not get to have no distortion. You get to choose where to put it. The slack in a paragraph has to live somewhere: in the right margin, where nobody has ever complained about it, or in the word spaces, where they complain constantly without knowing that is what they are describing.

Footnotes

  1. Robert Bringhurst, The Elements of Typographic Style, Hartley and Marks, first edition 1992. The sentence everybody quotes and nobody finishes: anything from 45 to 75 characters is widely regarded as a satisfactory length of line for a single-column page set in a serifed text face in a text size, and the 66-character line, counting both letters and spaces, is widely regarded as ideal. He gives 40 to 50 for multiple columns. Note the conditions he attached. People cite the number at a sans on a phone and then argue with each other about it. ↩

On this page

  • How the box gets filled
  • The same paragraph, two ways
  • Rivers
  • What has changed since I last complained about this
  • What I set