Line Breaking and Overflow: white-space, overflow-wrap, word-break, hyphens, text-wrap: balance/pretty and Multi-Line Truncation
Use overflow-wrap: anywhere for long URLs and ISBNs, white-space: pre-line for user text, text-wrap: balance for headings, and the -webkit-line-clamp recipe for multi-line truncation; do not rely on hyphens: auto for Turkish text. On Bookstore's book detail page we measure the min-content difference between break-word and anywhere, Turkish hyphenation failing in both engines, balance and pretty results, and WebKit not painting ::first-letter in a clamped box.
Version note: This post follows the CSS Text Module Level 3/4, CSS Overflow Level 4 and CSS Inline Layout Level 3 specifications and the November 2026 state of Baseline (web-features 3.46.0); the examples were run in Chromium 158 and WebKit 27.2 (Playwright 1.65), and Firefox support comes from web-features and MDN browser-compat-data 8.1.9.
Guess First
We added customer reviews to the book detail page of Bookstore, the online bookstore used as the shared example across this site's series. A customer pasted a long URL into a review and the review box overflowed to the right. We have two candidates to prevent the overflow: overflow-wrap: break-word and overflow-wrap: anywhere. We put both into a box that sizes to its content (min-content):
.row { display: flex; gap: 1rem; align-items: start; }
.fit { inline-size: min-content; }
.break-word { overflow-wrap: break-word; }
.anywhere { overflow-wrap: anywhere; }
<div class="row">
<p class="fit break-word">https://bookstore.test/books/9780141439549/reviews</p>
<p class="fit anywhere">https://bookstore.test/books/9780141439549/reviews</p>
</div>
Are the two boxes equally wide? Doesn't break-word already mean "break in the middle of the word"? Write your answer down; the first measurement is about it.
overflow-wrap and word-break: Long URLs, ISBNs and E-mail Addresses
Normally the browser breaks a line only at allowed places: spaces, hyphens and some punctuation according to the language's rules. A long URL, ISBN or e-mail address without spaces overflows when it is wider than its box. There are three tools for this case, and they behave differently:
node scripts/03-wrap.mjs
fit break-word box width 386.8px
fit anywhere box width 11.8px
narrow break-word 4 lines (collapse / wrap)
narrow anywhere 4 lines (collapse / wrap)
narrow break-all 3 lines (collapse / wrap)
The answer to the opening question is in the first two lines. The break-word box is 386.8px: as wide as the whole URL. The anywhere box is 11.8px, the width of a single character. Both break in the middle of a word to prevent overflow, but only anywhere takes those break points into account when the box's min-content width is calculated. The difference shows up everywhere something sizes to its content, such as min-content, fit-content, min-width: auto in Flexbox and auto tracks in Grid: with break-word a flex item grows as wide as the URL and pushes the card, with anywhere it can shrink. The concept of intrinsic size itself was covered in post 03 of the CSS3 series.
In a fixed-width column, break-word and anywhere give the same result (4 lines) because the width comes from outside. word-break: break-all is different: it breaks any word in the middle when needed, even if nothing would overflow. Lines fill up completely, so it goes down to 3 lines, but ordinary words such as "See" and "for" can now be split at the end of a line too:
.narrow { inline-size: 12em; }
.break-all { word-break: break-all; }
For the review text in Bookstore we choose overflow-wrap: anywhere: it does not touch ordinary words, breaks only the part that does not fit, and lets the card shrink. I almost never use word-break: break-all for text in the Latin alphabet; its place is data such as code or hashes shown character by character in a column. The word-break: break-word value seen in older code is non-standard and deprecated; browsers treat it like overflow-wrap: anywhere.
white-space and Its Longhands: Keeping the User's Line Breaks
In HTML, consecutive spaces and line breaks collapse into a single space. If the customer wrote the review as three lines, it shows up as one paragraph:
<p class="review">Great translation.
Shipping took two days.
Would buy again.</p>
<p class="review pre">Great translation.
Shipping took two days.
Would buy again.</p>
.review { inline-size: 24em; }
.review.pre { white-space: pre-wrap; }
review 2 lines (collapse / wrap)
review pre 3 lines (preserve / wrap)
Normally the text wraps into 2 lines; with pre-wrap the 3 lines the customer wrote are kept. The values in parentheses are the new longhands of white-space: white-space is now a shorthand for white-space-collapse (whether spaces collapse or are preserved) and text-wrap-mode (whether lines wrap). pre-wrap expands to preserve and wrap; nowrap to collapse and nowrap. white-space-collapse has been Baseline widely available since September 2026, so writing the longhand on its own is safe too.
A short summary of the values: normal collapses spaces and line breaks and wraps; nowrap collapses but does not wrap; pre preserves everything and does not wrap; pre-wrap preserves everything and wraps; pre-line preserves line breaks, collapses spaces and wraps; break-spaces is like pre-wrap but spaces at the end of a line also take up room. For text the user wrote I prefer pre-line: line breaks stay, accidental double spaces collapse.
hyphens: Hyphenation Depends on the Language
hyphens: auto lets the browser split a word at a syllable boundary using a language dictionary and insert a hyphen. The language is read from the element's lang attribute; without lang there is no hyphenation at all. I tried an English and a Turkish sentence in narrow 9em columns; the numbers are the pixel widths of each line:
.hyphen { inline-size: 9em; hyphens: auto; }
.hyphen.manual { hyphens: manual; }
--- chromium
hyphen lang=en 5 lines: 128 141 84 81 101
hyphen manual lang=en 5 lines: 128 141 58 97 101
hyphen lang=tr 5 lines: 158 103 60 114 137
hyphen manual lang=tr 5 lines: 158 103 60 114 137
hyphen manual shy lang=tr 5 lines: 134 137 60 114 137
--- webkit
hyphen lang=en 5 lines: 128 141 84 139 52
hyphen manual lang=en 5 lines: 128 141 58 97 101
hyphen lang=tr 5 lines: 158 103 60 114 137
hyphen manual lang=tr 5 lines: 158 103 60 114 137
hyphen manual shy lang=tr 5 lines: 134 137 60 114 137
Both engines hyphenate English: with manual the third line stays at 58px, with auto "ex-" is added and it grows to 84px. Which word is split where depends on the engine; WebKit distributed the last two lines differently. In Turkish there is no difference at all between auto and manual in either engine, and the first line overflows the 144px column at 158px: "Kütüphanelerimizdeki" was not split. According to browser-compat-data Chrome has no Turkish hyphenation dictionary; Firefox and Safari are listed as supporting Turkish. Playwright's WebKit build did not hyphenate Turkish on this machine, while the same build hyphenated a German word. I could not separately test Turkish in the released Safari or in Firefox.
The hyphens property has been widely available since March 2026, but that is true only for the property itself. Language support differs from browser to browser and is missing in Chrome, which most Turkish users use. Do not rely on hyphens: auto for Turkish text. If you know where a long word should be split, you can mark it with ­, the soft hyphen that appears only when the line breaks at that point; hyphens: manual is already the default:
<p class="hyphen manual shy" lang="tr">Kütüp­hane­lerimiz­deki karşılaştırmalı edebiyat koleksiyonlarını güncelleyemiyoruz.</p>
On the last line the first line went down to 134px and fits the column: "Kütüphanelerimiz-" is written and the rest moves to the next line. For text you do not know in advance, such as user content, the safety net is overflow-wrap: anywhere.
text-wrap: balance and pretty
text-wrap is the shorthand for text-wrap-mode and text-wrap-style. On the style side, two values are useful in everyday work. balance distributes lines at similar lengths, while pretty tries to avoid a single word left at the end of a paragraph:
.title { inline-size: 20em; font-size: 24px; margin: 0; }
.title.balance { text-wrap: balance; }
.para { inline-size: 20em; }
.para.pretty { text-wrap: pretty; }
--- chromium
title line widths: 457 404 79
title balance line widths: 327 236 377
para line widths: 307 299 311 49
para pretty line widths: 307 299 290 70
--- webkit
para line widths: 307 299 311 49
para pretty line widths: 256 233 241 235
In a long book title without balance, "World" is left alone on the last line (79px). With balance the shortest line grows to 236px and the title reads like three balanced lines; the total width does not change, only the break points do. It is normal that the lines are not exactly equal: breaks can only happen between words. Both engines gave the same result for this title.
With pretty the engines diverge. The normal paragraph has a single word on its last line (49px). With pretty, Chromium fixes only the last lines and pulls one more word onto the last line (70px); WebKit redistributes the whole paragraph and brings all four lines closer together. The specification leaves the algorithm to the browser, so both results are valid. balance has been Baseline newly available since May 2024; pretty exists in Chrome 117 and Safari 26 but not in Firefox, so it is not Baseline. Browsers without support ignore the value, so it can be written directly as progressive enhancement. We added this to Bookstore's base styles:
h1, h2, h3 { text-wrap: balance; }
p { text-wrap: pretty; }
For performance reasons Chromium limits balance to blocks below a certain number of lines; Chrome's documentation gives that limit as six lines. Writing balance on a long paragraph therefore either does nothing or adds needless cost; keep it for headings, labels and short quotes.
Multi-Line Truncation: -webkit-line-clamp
Cutting single-line text with text-overflow: ellipsis was covered in post 03 of the CSS3 series. To cut the book description at three lines and add "…" at the end, we still use an old, prefixed syntax:
.summary { inline-size: 20em; }
.clamp {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}
.clamp-block { -webkit-line-clamp: 3; overflow: hidden; }
--- chromium
summary clamp height 72px (line-height 24px)
summary clamp-block height 192px (line-height 24px)
supports: line-clamp false, -webkit-line-clamp true, text-wrap: pretty true, text-box true
--- webkit
summary clamp height 72px (line-height 24px)
summary clamp-block height 192px (line-height 24px)
supports: line-clamp false, -webkit-line-clamp true, text-wrap: pretty true, text-box true
The .clamp box is 72px, three 24px lines. .clamp-block gets the same -webkit-line-clamp: 3 and still shows all eight lines at 192px: -webkit-line-clamp only works together with display: -webkit-box and -webkit-box-orient: vertical. This four-line recipe is part of the old flexbox syntax from 2009, but today it is the only way that works consistently in all three engines. The unprefixed line-clamp has not shipped in any browser: both engines returned false for CSS.supports('line-clamp: 3'), and according to browser-compat-data it exists only behind an experimental flag in development builds of Chrome and Firefox, and only in Safari Technology Preview.
In Bookstore the summary paragraph is cut at three lines by default, with a "Read more" button below it:
/* three lines until "Read more" is pressed */
.book-summary.is-clamped {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}
.review-text {
/* keep the customer's line breaks, collapse extra spaces */
white-space: pre-line;
/* long URLs and ISBNs may break anywhere */
overflow-wrap: anywhere;
}
<p class="book-summary is-clamped" id="summary">Middlemarch follows the lives of the people of a small
English town in the years before the 1832 Reform Act: Dorothea Brooke, who marries an elderly
scholar, the young doctor Tertius Lydgate, who wants to reform medicine, and the families around
them, whose ambitions and mistakes slowly change the town. George Eliot published the novel in
eight parts in 1871 and 1872.</p>
<button type="button" class="read-more" aria-controls="summary" aria-expanded="false" hidden>Read more</button>
// Shows "Read more" only when the clamped summary really hides text.
for (const button of document.querySelectorAll('.read-more')) {
const summary = document.getElementById(button.getAttribute('aria-controls'));
const update = () => {
if (summary.classList.contains('is-clamped')) {
button.hidden = summary.scrollHeight <= summary.clientHeight;
}
};
// runs again after styles, fonts or the viewport width change the layout
new ResizeObserver(update).observe(summary);
button.addEventListener('click', () => {
const expanded = summary.classList.toggle('is-clamped') === false;
button.setAttribute('aria-expanded', String(expanded));
button.textContent = expanded ? 'Show less' : 'Read more';
});
}
The button appears only when the summary is really cut: in a clamped element scrollHeight includes the hidden lines and is larger than clientHeight. Doing this comparison inside a ResizeObserver matters. In my first attempt I ran the check as soon as the script ran, and in WebKit the button stayed hidden; at that moment the stylesheets and the font had not settled yet. ResizeObserver also repeats the check when the font changes or the screen rotates.
node scripts/03-pages.mjs
--- chromium
clamped: height 76.78125px, button "Read more" aria-expanded=false
red ::first-letter pixels while clamped: 899
text in the DOM: 62 words
expanded: height 204.75px, button "Show less" aria-expanded=true
red ::first-letter pixels when expanded: 899
review: 4 lines, scrollWidth 372 = clientWidth 372
--- webkit
clamped: height 76.78125px, button "Read more" aria-expanded=false
red ::first-letter pixels while clamped: 0
text in the DOM: 62 words
expanded: height 205px, button "Show less" aria-expanded=true
red ::first-letter pixels when expanded: 70
review: 4 lines, scrollWidth 372 = clientWidth 372
The measurement shows several things. The clamped summary is 76.8px, 205px when expanded; the button updates aria-expanded correctly. Even when clamped, all 62 words of the text stay in the DOM: screen readers and search engines read the full text, it is only cut visually. The review text has 4 lines and its scrollWidth equals its clientWidth, so the long URL does not overflow.
An engine difference I did not expect showed up too. In the previous post we made the first letter of the summary paragraph red with ::first-letter. When clamped, Chromium paints that letter (899 red pixels) while WebKit does not paint it at all (0); once the summary is expanded and display: -webkit-box is gone, the letter turns red in WebKit too. WebKit does not apply ::first-letter in a -webkit-box box. If you put other pseudo-element decoration on a clamped element, check it in both engines.
text-box: A Quick Look at Vertical Alignment in a Button
Text inside a button often does not sit exactly in the middle because of the font's ascent and descent area and the space line-height adds: more space is left above capital letters than below them. text-box trims this excess at the top and bottom of the line box based on the font's metrics:
.btn { font: 600 16px/1.5 system-ui, sans-serif; padding: 0.5em 1em; border: 0; }
.btn.trim { text-box: trim-both cap alphabetic; }
btn height 40.0px
btn trim height 27.3px
trim-both cap alphabetic trims the top to the cap height and the bottom to the baseline; the button went down from 40px to 27.3px, and the padding is now measured from the visible letters. This is the CSS counterpart of the "trim the text box to the letters" setting in design tools. The support picture is mixed: in web-features 3.46 text-box is not Baseline. browser-compat-data shows the properties in Firefox 154 (August 2026), but because Firefox support is not marked on value entries such as cap and alphabetic, the feature as a whole is not counted as Baseline. I could not run Firefox on this machine, so I could not verify it myself; without support the button only looks a little taller, so it is safe to write as progressive enhancement.
In Production
The results of line-breaking rules depend on the content, and test data is usually short and tidy. Put edge cases that represent real data (a very long book title, a URL without spaces, a long Turkish word, a review with line breaks) on a test page and measure the line count and scrollWidth on every change, as in this post. A card overflowing is usually noticed in production, because of a review a customer wrote.
text-wrap: balance and pretty add work to the layout calculation. For headings this is negligible; before applying pretty to every paragraph of long lists and frequently updated content, look at the layout time in the Performance panel of DevTools.
For truncated text to be accessible, the full text must stay in the DOM (ours does) and "Read more" must be a real button. If you truncate the text on the server by character count, the only way to reach the full text is another request.
Common Pitfalls
First, applying text-wrap: balance to long paragraphs. Chromium stops balancing after a certain number of lines, so the expected result does not come and the cost is wasted.
Second, leaving no way to reach the full version of text cut with line clamp. Visually truncated text is lost for keyboard and mouse users; add an expand button and aria-expanded.
Third, mistaking text that overflows inside a flex item for a line-breaking problem. A flex item's default min-width: auto does not let it become narrower than its content; overflow-wrap: break-word does not help here, you need to give the item min-width: 0 or use anywhere. The mechanism is in post 05 of the CSS3 series.
Fourth, writing the text of an element with pre-wrap on a new, indented line after the tag in a template. The line break and indentation in the template are preserved too and show up as an empty line at the start of the review; write the text right after the opening tag.
Baseline Status
node scripts/baseline.mjs white-space white-space-collapse text-wrap overflow-wrap word-break hyphens text-wrap-balance text-wrap-pretty line-clamp text-box text-overflow
white-space widely 2018-01-29 Chrome 1, Firefox 1, Safari 1, iOS 1
white-space-collapse widely 2026-09-19 Chrome 114, Firefox 124, Safari 17.4, iOS 17.4
text-wrap newly 2024-10-17 Chrome 130, Firefox 124, Safari 17.5, iOS 17.5
overflow-wrap widely 2021-04-02 Chrome 23, Firefox 49, Safari 7, iOS 7
word-break widely 2018-03-30 Chrome 44, Firefox 15, Safari 9, iOS 9
hyphens widely 2026-03-18 Chrome 88, Firefox 43, Safari 17, iOS 17
text-wrap-balance newly 2024-05-13 Chrome 114, Firefox 121, Safari 17.5, iOS 17.5
text-wrap-pretty not Baseline Chrome 117, Firefox -, Safari 26, iOS 26
line-clamp not Baseline Chrome -, Firefox -, Safari -, iOS -
text-box not Baseline Chrome 133, Firefox -, Safari 18.2, iOS 18.2
text-overflow widely 2018-01-29 Chrome 1, Firefox 7, Safari 1.3, iOS 1
| Feature | Status | Note |
|---|---|---|
white-space, overflow-wrap, word-break, text-overflow | Widely available | The anywhere value is in all three engines too |
white-space-collapse | Widely available | Since 2026-09 |
hyphens | Widely available | No Turkish dictionary in Chrome |
text-wrap, text-wrap: balance | Newly available | For headings |
text-wrap: pretty | Not Baseline | No Firefox; progressive enhancement |
Unprefixed line-clamp | Not Baseline | Not shipped; use the -webkit-line-clamp recipe |
text-box | Not Baseline | Chrome and Safari; Firefox data incomplete |
A Small Experiment
★☆☆ Force the lab's .fit boxes to 200px inside a flex container (give .row inline-size: 200px) and measure with 03-wrap.mjs how the widths of the two boxes change.
★★☆ Make the review text on the book detail page pre-wrap and start the text on the next line in the template. Show the empty line from the pitfalls section with a measurement, then compare the difference with pre-line.
★★★ Try building the "Read more" button with details/summary without writing any JavaScript. Compare, with Playwright's ariaSnapshot() method, whether the text is visible in the DOM and in the accessibility tree in the clamped and expanded states.
In the next post we move from text to the look of the box: the background longhands and what the shorthand resets, background-size and position, multiple layers, linear, radial and conic gradients, image-set() and the campaign banner.