Skip to content

Chapter V

Wrapping

How text breaks, wraps and overflows and why it should never be clipped.

  • Clipping content is data loss.
  • If you must clip, give people a way to see the rest.
  • Choose wrapping behavior to suit the kind of content.
  • Use overflow-wrap: anywhere as a default.

If content is on the page, people should be able to read it. If a design cuts content off, redesign the area so the content stays readable. Usually, that means planning for more content than you expect and letting it wrap. Cutting text off with an ellipsis (…) hides it from some readers.

The usual tools for this are text-overflow: ellipsis, which cuts off a single line, and line-clamp (written -webkit-line-clamp in most browsers today), which cuts off after a set number of lines. Both hide the rest of the text with no way to reach it.

For general text, I’d go as far as recommending overflow: visible !important as the default setting for text. It prevents data loss, and it makes anyone trying to hide content stop and think twice.

There’s one exception: you may clip content if there’s a way to reveal it. Clipped text has traditionally been marked with an ellipsis but, the ellipsis usually doesn’t look interactive. Here’s a pattern to try:

<details>
<summary>view</summary>
<!-- Put your text content here -->
</details>

This uses the native <details> and <summary> elements, which show and hide content. The button says “view” instead of relying on an ellipsis. One word works for both showing and hiding the content. A real word also gives people using voice control a visible name to say instead of an ambiguous symbol.

details {
--max-lines: 3;
container-type: scroll-state;
max-block-size: calc(var(--max-lines) * 1lh);
overflow: hidden;
text-align: justify;
hyphens: auto;
&::details-content {
content-visibility: visible;
display: block;
}
&::before {
content: "";
float: inline-end;
block-size: calc((var(--max-lines) - 1) * 1lh);
}
&[open] {
display: flex;
flex-direction: column;
max-block-size: none;
&::before {
display: none;
}
summary {
order: 1;
align-self: flex-end;
&::before {
content: "×" / ""; /* open: "× view" */
}
}
}
@media print {
max-block-size: none;
}
}
summary {
float: inline-end;
clear: both;
display: block;
margin-inline-start: 1ch;
color: LinkText;
text-decoration: underline;
list-style: none;
cursor: pointer;
&::-webkit-details-marker {
display: none;
}
&::before {
display: inline-block;
margin-inline-end: 0.5ch;
content: "…" / ""; /* closed: "… view" */
}
@media print {
display: none;
}
}
@container not scroll-state(scrollable: block) {
details:not([open]) summary {
display: none;
}
}

There’s a lot here, and those comfortable with CSS should follow much of it. Here’s what the less familiar parts of the CSS do:

  • --max-lines: 3; shows at most three lines when collapsed.
  • container-type: scroll-state; and the later scroll-state(scrollable: block) check whether any content is hidden. If there are 3 lines or fewer, there’s nothing more to view, so the “view” button isn’t shown.
  • The lh unit equals one line height, so 3lh is exactly 3 lines of text tall. This holds as long as the line height isn’t changed inside the element. If a reader overrides the line height, as WCAG 1.4.12 (Text Spacing) allows, lh follows it, so the collapsed area still shows 3 full lines.
  • text-align: justify; helps place the floated <summary> right after the last word of the collapsed text. Without it, the <summary> sits at the far right, where the last line of text may not reach. text-align: start; may also work.
  • LinkText is a system color for interactive elements inside text, like links. The underline is another hint that it can be clicked. You can use your own styles for this.
  • order: 1; moves the <summary> to the end of the passage. Using order is usually discouraged, because the visual order no longer matches the order in the HTML. It’s acceptable here if this is the only interactive element in the passage.
  • Screen readers skip the text in the ::before elements, but it helps sighted users see what “view” will do. Screen readers announce “view, collapsed” or “view, expanded” on their own.

As with any accessibility pattern, test it yourself to see if it meets your users’ needs. And of course, it’s simpler to show all the content by default than to make people click to see the rest.

The browser decides where each line of text ends and we can suggest how these decisions are made with CSS. Their names are similar, and some overlap, so it’s easy to mix them up. We’ll go through them one at a time.

First, a few terms these properties use:

  • Whitespace: characters that add space between other characters, like spaces and tabs.
  • Segment breaks: characters that start a new line, like a line feed.
  • Hard breaks: forced line breaks. They always break, whether or not it’s needed.
  • Soft breaks: breaks that happen only when needed. The places they can happen are called soft wrap opportunities.
  • CJK: Chinese, Japanese, and Korean, which have different rules about where a line may break.

This property has been around a long time, but it has since been split into separate properties. Together, they control how whitespace characters affect the surrounding text.

The first is white-space-collapse, which has these values:

  • collapse merges runs of whitespace together. The exact rules take several steps.
  • preserve keeps whitespace and segment breaks as written. This is the default for the <pre/> HTML element.
  • preserve-breaks keeps segment breaks but collapses other whitespace.
  • preserve-spaces does the opposite: it keeps whitespace as written but collapses segment breaks.
  • break-spaces keeps whitespace like preserve, but lets lines wrap after any space, and spaces at the end of a line still take up room.

white-space-collapse is only one half of white-space. The other half is text-wrap-mode, which has just two values: wrap and nowrap. You’ll almost always want text-wrap-mode: wrap, which is the default.

Note

When would you want text-wrap-mode: nowrap? The <pre/> element uses it to keep code formatted and readable. Other uses include:

  • Keeping table data in dense rows.
  • Keeping key phrases on one line, eg., “Google LLC”.
  • Controlling hidden text for assistive technologies.

The original white-space keywords don’t obviously match these new values. Here’s how they map:

white-space white-space-collapse text-wrap-mode
normal = collapse + wrap
pre = preserve + nowrap
pre-line = preserve-breaks + wrap
pre-wrap = preserve + wrap

Confusingly, text-wrap is also a shorthand. We’ve already seen one part, text-wrap-mode, which sets whether text can wrap. The newer part is text-wrap-style, which adds smarter ways to choose where lines break. Its values are:

  • auto wraps text the fastest way, without comparing line lengths. This is the default.
  • balance tries to keep lines roughly the same length. It’s best for headlines. Balancing is slow on long text, so browsers only do it for short blocks: up to 6 lines in Chromium and 10 in Firefox. Longer text wraps as if it were auto.
  • pretty trades a little speed for a better layout, such as avoiding a single word alone on the last line. It’s best for paragraphs.
  • stable is for text the user is editing. It keeps earlier lines from shifting while they type.

Harry Roberts posted some benchmarks for the newer text-wrap values. His test page had 10,000 <p/> elements, each ending with an orphan (a single word alone on the last line) by default. Here’s what he found:

  • Baseline: 1,186ms
  • text-wrap: balance: 1,224ms (38ms, or 3.2%, longer)
  • text-wrap: pretty: 1,310ms (124ms, or 10.5%, longer)

He said the cost of balance was almost nothing, while pretty was more noticeable. Realistically, your page won’t have 10,000 paragraphs.

overflow-wrap tells the browser when it may break a long string that would otherwise overflow its box. It’s another name for the word-wrap property. It has three values:

  • normal, the default, keeps strings whole.
  • break-word breaks a string only when it can’t fit on a line of its own. This is very helpful for long URLs, which would otherwise spill out of the text. It’s so helpful that it often appears in CSS resets.
  • anywhere breaks strings the same way, with one important difference.

The difference shows up in boxes that size themselves to their content, like flex items, grid items, table cells, and inline-block elements. To size these, the browser works out the narrowest the box can get. With break-word, it ignores the places a string could break, so the narrowest the box can get is the width of the longest string. The box resizes to fit a long URL instead of wrapping it, and the URL still spills out of the layout. With anywhere, the browser counts those places, so the box can shrink to fit its container and the URL wraps.

Since anywhere handles every case break-word does, plus the boxes that size to their content, I recommend overflow-wrap: anywhere as the default. This article on overflow-wrap walks through the difference with examples.

Continuing the theme of confusingly similar properties, word-break sets what happens when a string would overflow its container. Worse, one of its values overrides overflow-wrap. We’ll start with that one:

  • break-word is the same as overflow-wrap: anywhere with word-break: normal, and it overrides any overflow-wrap value. It’s deprecated, so set overflow-wrap: anywhere instead.
  • normal uses the usual line break rules, which may break between CJK characters.
  • break-all breaks at the point where the text would overflow.
  • keep-all is the same as normal, except it doesn’t break between CJK characters either.
  • auto-phrase is the same as normal, but analyzes the language to avoid breaking up natural phrases.

This property is mostly used for CJK text, which has more detailed rules about which characters may sit next to each other.

  • auto uses the default rules for the language (set by the lang attribute) and the operating system’s text rendering. This is the default.
  • loose uses the least strict rules. Often used for headlines.
  • normal uses the most common rules, as defined by the Unicode standard.
  • strict uses the strictest rules. Often used for official documents, books, and anywhere typographic standards must be followed.
  • anywhere allows a break between any two characters, regardless of other settings.
Note

If your content includes Japanese, the overflow-wrap article linked above pairs overflow-wrap: anywhere with line-break: strict. The strict rules keep small kana and punctuation from starting a line, which reads better in Japanese text.

This property sets whether the browser may break a word across two lines with a hyphen.

  • none never hyphenates, even where you’ve marked a spot.
  • manual hyphenates only where you’ve marked a spot with a soft hyphen (see below). This is the default.
  • auto lets the browser hyphenate words using a dictionary for the text’s language.

Hyphenation helps most in narrow columns and justified text, where long words would otherwise leave large gaps. The disclosure pattern above uses hyphens: auto for this reason.

Several of these properties depend on the language of the text. The browser uses the lang attribute to pick its hyphenation dictionary, its line-break rules, and the phrases auto-phrase keeps together. Without lang, hyphens: auto does nothing.

Set it on the html element, and again on any passage in a different language:

<html lang="en">
<!-- … -->
<p lang="fr">Une phrase en français.</p>
</html>

Sometimes you know better than the browser where a long string should break. HTML has two ways to mark the spot:

  • <wbr> marks a place where a line may break, without adding a hyphen. It’s useful in long URLs, file paths, and code names.
  • &shy;, the soft hyphen, marks a place where a word may break. If the line breaks there, a hyphen is shown. If not, nothing is shown.
<p>https://example.com/<wbr>a-very/<wbr>long-path</p>
<p>Extra&shy;ordinary</p>