Justifying text-wrap: pretty

Something truly monumental happened in the world of software development in 2025. Safari shipped a reasonable implementation of text-wrap: pretty: https://webkit.org/blog/16547/better-typography-with-text-wrap-pretty/. We are getting closer and closer to the cutting-edge XV-century technology. Beautiful paragraphs!

Gutenberg bible Old Testament Epistle of St Jerome

We are not quite there yet, hence the present bug report.


A naive way to break text into lines to form a paragraph of a given width is greediness: add the next word to the current line if it fits, otherwise start a new line. The result is unlikely to be pretty — sometimes it makes sense to try to squeeze one more word on a line to make the lines more balanced overall. Johannes Gutenberg did this sort of thing manually, to produce a beautiful page above. In 1981, Knuth and Plass figured out a way to teach computer to do this, using dynamic programming, for line breaking in TeX.

Inexplicably, until 2025, browsers stuck with the naive greedy algorithm, subjecting generations of web users to ugly typography. To be fair, the problem in a browser is harder version than the one solved by Gutenberg, Plass, and Knuth. In print, the size of the page is fixed, so you can compute optimal line breaking once, offline. In the web context, the window width is arbitrary and even changes dynamically, so the line-breaking has to be “online”. On the other hand, XXI century browsers have a bit more compute resources than we had in 1980 or even 1450!


Making lines approximately equal in terms of number of characters is only half-way through towards a beautiful paragraph. No matter how you try, the length won’t be exactly the same, so, if you want both the left and the right edges of the page to be aligned, you also need to fudge the spaces between the words a bit. In CSS, text-wrap: pretty asks the browser to select line breaks in an intelligent way to make lines roughly equal, and text-align: justify adjusts whitespace to make them equal exactly.

Although Safari is the first browser to ship a non-joke implementation of text-wrap, the combination with text-align looks ugly, as you can see in this very blog post. To pin the ugliness down, the whitespace between the words is blown out of proportion. Here’s the same justified paragraph with and without text-wrap: pretty:

The paragraph happens to look ok with greedy line-breaking. But the “smart” algorithm decides to add an entire line to it, which requires inflating all the white space proportionally. By itself, either of

p {
    text-wrap: pretty;
    text-align: justify;
}

looks alright. It’s just the combination of the two that is broken.


This behavior is a natural consequence of implementation. My understanding is that the dynamic programming scoring function aims to get each line close to the target width, and is penalized for deviations. Crucially, the actual max width of a paragraph is fixed: while a line can be arbitrary shorter, it can’t be any longer, otherwise it’ll overflow. For this reason, the dynamic programming sets the target width to be a touch narrower than the paragraph. That way, it’s possible to both under and overshoot, leading to better balance overall. As per original article:

The browser aims to wrap each line sooner than the maximum limit of the text box. It wraps within the range, definitely after the magenta line, and definitely before the red line.

But if you subsequently justify all the way to the red line, the systematic overshoot will manifest itself as too wide inter-word space!

WebKit devs, you are awesome for shipping this feature ahead of everyone else, please fix this small wrinkle such that I can make my blog look the way I had intended all along ;-)