Where a Line May Break: The Unicode Algorithm Behind Every Wrapped Paragraph
Published 7/1/2025 · 13 min read · Text & language tools
Daniel Okonkwo — Front-end developer and tech writer at Allin
Web performance · File formats
Checked against 5 sources
Wrapping text by splitting on spaces is wrong in most of the world's writing systems, and Unicode specifies the correct procedure: UAX #14, the line breaking algorithm. It assigns every code point a line-break class and then applies numbered rules to each pair of classes. A no-break space U+00A0 and a narrow no-break space U+202F are class GL, glue, and a break is forbidden after them: a conforming implementation found zero break opportunities inside cost·100·USD written with no-break spaces, where the ordinary version has two. A soft hyphen U+00AD is class BA, so a break is allowed there and the hyphen only becomes visible if the break is taken. A zero-width space U+200B is class ZW: it permits a break and shows nothing, which is how text with no spaces at all gets wrapped. A non-breaking hyphen U+2011 is glue, while an ordinary hyphen-minus U+002D is class HY and does allow a break after it. Japanese needs no spaces because every ideograph is class ID and a break is allowed between almost any two: our 18-character sentence had 16 break opportunities and no spaces. Thai is class SA and has none at all without dictionary analysis — the same sentence gained three opportunities when three zero-width spaces were inserted.
"Break at spaces" fails in most of the world's writing systems. UAX #14 gives every character a line-break class; we looked ours up in Unicode 17.0.0 and ran a conforming implementation over no-break spaces, soft hyphens, zero-width spaces, URLs, Japanese and Thai.
"Break at spaces" describes one family of scripts
Split on the space character and a Japanese sentence comes back as one token: 日本語のテキストは、空白で区切らない has no spaces at all, and neither does the Thai equivalent. Both wrap perfectly well on screen, because the rule is not about spaces. Run a conforming line-breaking implementation over that Japanese sentence and it reports sixteen break opportunities in eighteen characters — one between almost every pair — while refusing to break before the ideographic comma or the full stop. The same implementation over the Thai sentence reports zero, for a reason we come back to below.
The Latin-script case is not safe either. Written with ordinary spaces, cost 100 USD offers two break opportunities; written with no-break spaces at U+00A0 it offers none, and a space-splitting wrapper returns it as one long token it cannot break. The characters look identical. The difference lives in the code points, and any wrapper that reads only the shapes will produce a line that is technically too long or a break that is typographically wrong.
UAX #14: a class per character, then rules on pairs
The Unicode Character Database ships a file called LineBreak.txt that assigns a two-letter class to every code point. We parsed the current one — LineBreak-17.0.0.txt, dated 29 July 2025 — and it defines 49 distinct classes across 3,654 ranges. The largest by far is ID, ideographic, at 172,561 code points; AL, ordinary alphabetic, covers 26,954; the interesting ones are tiny, with GL at 41 code points, QU at 39, WJ at 2 and ZW at exactly 1. The algorithm then walks the string and consults a rule for each adjacent pair of classes.
The rules are numbered and split into two groups. A handful are non-tailorable: an implementation may not change them and still claim conformance. The rest, from LB12a onward, are a reasonable default that an implementation is allowed to improve on. LB12 forbids a break after glue. LB13 forbids one before the closing bracket, the exclamation mark, the solidus and the closing punctuation class, even after spaces. LB18 allows a break after a space. LB21 forbids one before a hyphen or a break-after character. Where the pairs conflict, the lowest-numbered rule wins, which is why the specification is a list rather than a table.
Five invisible characters that decide where lines fall
The no-break space and the narrow no-break space are both class GL. They occupy width and forbid a break, which is what keeps a number with its unit and a title with its name. The word joiner U+2060 is class WJ and is stronger still: no break before it and none after, and it takes no width at all. The zero-width space U+200B is the opposite: class ZW, no width, break permitted. And the soft hyphen U+00AD is class BA, a break-after character that renders as nothing until the break is used, at which point the renderer supplies a hyphen.
The soft hyphen run makes the behaviour concrete. We took a 63-character German compound and inserted five soft hyphens, giving a stored length of 68 and a visible length of 63. At width 99 it renders as one unbroken word of 63 characters. At width 30 it renders on three lines, at width 20 on four, and the hyphen appears only at the end of each broken line — never in the middle. Strip the soft hyphens and the same word at width 20 comes back as a single 63-character line that overflows, because there is nowhere the algorithm is allowed to break.
The same trick rescues scripts the algorithm cannot segment on its own. Thai is class SA, complex context dependent, and UAX #14 states plainly that runs of these characters require morphological analysis, that no break opportunities will be found otherwise, and that an implementation without such analysis should treat them as ordinary letters. Our implementation reported zero opportunities in a 23-character Thai sentence. Inserting three zero-width spaces at the word boundaries produced exactly three — invisible in the rendered text, decisive for the wrap.
URLs break in places you did not choose
Feed a URL to the algorithm and the answer is more interesting than either "nowhere" or "anywhere". Our test address produced seven break opportunities: after the double solidus, after each single solidus, after each hyphen inside the path, and after the question mark. Never before a solidus — LB13 forbids that outright, even after a space — and never at the dots in the host name, because the full stop is class IS and LB15d refuses a break before it. The result is that a wrapped URL keeps its separators at the end of the line, where a reader can tell the line continues.
The danger is what you do with that break. A soft wrap is a display decision: the string in memory is unchanged, and re-flowing the paragraph at a different width simply moves the break. A hard wrap inserts a real line-feed character into the data. We hard-wrapped the test URL at 24 columns; rejoining the lines with nothing gave the URL back byte for byte, but rejoining them with a space — which is what a mail client, a chat app or a careless copy will do — produced four fragments separated by spaces, and the link was dead. Never hard-wrap anything that has to remain a single token.
Hard wrapping destroys reflow — measured
We wrapped a paragraph at 32 columns, saved the result with real newlines, then re-wrapped it at 20 without unwrapping first. The output was seven lines, several of them far shorter than 20 characters, because each hard line was re-wrapped in isolation and the ragged ends could not be filled. Unwrapping first — joining the lines back with a space — and then wrapping at 20 gave five full lines. That difference is the cost of hard wrapping: the text no longer knows which line ends were the author's and which were the previous renderer's.
This is also where email's famous fixed width comes from, and the number people quote is not the one in the specification. RFC 5322 says each line MUST be no more than 998 characters and SHOULD be no more than 78, excluding the CRLF; the 998 exists because transport implementations refuse more than 1000 characters per line, and the 78 exists because display software mangles anything longer. RFC 3676, which defines format=flowed, repeats the 78 and explains it is not 79 or 80 because the last column is often reserved for a wrap indicator. The 72 that mail clients actually used is a convention on top of that, leaving room for several levels of quote markers in replies.
Format=flowed is the compromise that made fixed-width mail survivable: a line ending in a space before its CRLF is a soft break the reader may re-flow, while a line ending without one is the author's. It is ordinary text to a client that has never heard of it, and re-wrappable to one that has. The general lesson holds beyond email: store the paragraph, not the lines, and let the last renderer decide where the breaks go.
What CSS lets you change, and what it does not
CSS Text Level 3 exposes the algorithm through five properties, and it is worth knowing their exact value sets. white-space takes normal, pre, nowrap, pre-wrap, break-spaces or pre-line, and defaults to normal. word-break takes normal, keep-all, break-all or break-word. line-break takes auto, loose, normal, strict or anywhere, and controls how strictly the algorithm is applied to East Asian punctuation. hyphens takes none, manual or auto, defaulting to manual. overflow-wrap takes normal, break-word or anywhere. The specification cites UAX #14 by name for the underlying classes.
Two details in that list are easy to get wrong. Because hyphens defaults to manual, soft hyphens already in your text are honoured without any CSS at all — and setting hyphens: none suppresses them, but the specification is explicit that it does not suppress the break opportunities offered by visible characters such as U+002D and U+2010. And overflow-wrap: anywhere is a last resort, not a fix: it allows a break at any character when a word would otherwise overflow, which for the 63-character compound above means a break in the middle of a morpheme where a soft hyphen would have given a correct one.
Typographic spaces are not decoration
French punctuation is the clearest demonstration that these code points do real work. We wrapped a French sentence twice: once with ordinary spaces everywhere, once with the correct narrow no-break spaces before the high punctuation and a no-break space before the unit. The first version offered fourteen break opportunities, including one between a number and its thousands group and one before a closing guillemet. The second offered nine, and none of them separated a mark from the word it belongs to. Same visible text, different set of legal line breaks.
The same reasoning applies far beyond French. A number and its unit, an abbreviation and the name after it, a figure and a percent sign, an ordinal and its noun: all of these read badly when a line break falls between them, and all of them are fixed by one invisible character rather than by a rule in the wrapper. Our text-wrapping tool leaves those characters alone, which is the only correct behaviour: cleaning them away, as a naive whitespace normaliser does, silently gives the renderer permission to break where the author forbade it.
| Character | Code point | Class | Break after? | Visible? |
|---|---|---|---|---|
| Space | U+0020 | SP | Yes | Yes, as blank width |
| No-break space | U+00A0 | GL | No | Yes, as blank width |
| Narrow no-break space | U+202F | GL | No | Yes, narrower |
| Hyphen-minus | U+002D | HY | Yes | Always |
| Non-breaking hyphen | U+2011 | GL | No | Always |
| Soft hyphen | U+00AD | BA | Yes | Only if the break is taken |
| Zero-width space | U+200B | ZW | Yes | Never |
| Word joiner | U+2060 | WJ | No, and none before it either | Never |
| Solidus in a URL | U+002F | SY | Yes after, never before (LB13) | Always |
| CJK ideograph | U+4E00 and 172,560 more | ID | Yes, between almost any two | Always |
| Thai letter | U+0E01 | SA | Only with dictionary analysis | Always |
Frequently asked questions
- Why does my text wrap in the middle of a number?
- Because the group separator is an ordinary space, class SP, and rule LB18 allows a break after any space. Replace it with a no-break space U+00A0 or a narrow no-break space U+202F, both class GL, and the opportunity disappears: our French sentence went from fourteen break opportunities to nine when the typographic spaces were used, and none of the remaining nine fell inside a number.
- What is the difference between a soft hyphen and a zero-width space?
- Both permit a break and neither is visible until it is used, but the soft hyphen U+00AD leaves a hyphen behind when the break is taken and the zero-width space U+200B leaves nothing. Use the soft hyphen inside a word, where a hyphen is the correct typography; use the zero-width space between units that should not gain a hyphen, such as parts of a long identifier or a URL, and in scripts that never hyphenate.
- Should I hard-wrap text at 72 or 80 characters?
- Only when a protocol requires it. RFC 5322 sets a hard limit of 998 characters per line and a recommendation of 78; the 72 that mail clients used leaves room for quote markers in replies and is a convention, not a specification. Hard wrapping costs reflow: re-wrapping a paragraph we had already hard-wrapped at 32 columns produced seven ragged lines at width 20, against five full ones when we unwrapped it first.
- How do I stop a long URL from overflowing its container?
- Not by inserting newlines. The algorithm already offers breaks after the double solidus, after each solidus and after each hyphen — seven opportunities in our test address — so a soft wrap usually suffices. If the container is narrower than the longest unbreakable run, add overflow-wrap: anywhere for that element or insert zero-width spaces, which cost nothing when copied. A hard-wrapped URL rejoined with a space is no longer a URL.
- Does the algorithm know where Thai words end?
- No, and UAX #14 says so. Thai, Lao and Khmer characters are class SA, complex context dependent, and the annex states that runs of them require morphological analysis, that no break opportunities will be found otherwise, and that an implementation lacking such analysis should treat them as ordinary letters. Our conforming implementation found zero opportunities in a 23-character Thai sentence, and exactly three once three zero-width spaces were inserted.
Articles you may find interesting
All guides →Related tools
Sources
- Unicode Consortium — Unicode Standard Annex #14: Unicode Line Breaking Algorithm — line break classes and rules LB1–LB31
- Unicode Consortium — LineBreak.txt in the Unicode Character Database — the authoritative class for every code point (17.0.0, 2025-07-29)
- W3C — CSS Text Module Level 3 — white-space, word-break, line-break, hyphens and overflow-wrap
- IETF (RFC Editor) — RFC 5322 — Internet Message Format, section 2.1.1 Line Length Limits (998 characters MUST, 78 SHOULD)
- IETF (RFC Editor) — RFC 3676 — The Text/Plain Format and DelSp Parameters (format=flowed)
Spotted a mistake in this article?