Beyond `text-wrap: pretty` — language-aware line breaks for minor words

Ever noticed that tiny “a” dangling at the end of a headline? 😱

text-wrap: pretty already makes paragraphs look smarter — but it isn’t language-aware. In many editorial traditions, you don’t want to break after minor words (articles, prepositions, con…


This content originally appeared on DEV Community and was authored by Jacopo Lorenzetti

Ever noticed that tiny “a” dangling at the end of a headline? 😱

text-wrap: pretty already makes paragraphs look smarter — but it isn’t language-aware. In many editorial traditions, you don’t want to break after minor words (articles, prepositions, conjunctions), or between an honorific and the following name, or between initials and a surname.

This post proposes a small, language-aware upgrade to line breaking — and ships a tiny polyfill you can try in 30 seconds.

With only text-wrap: pretty, the orphan is gone — but pairs like Fig./2 and J. K./Rowling can still split. The language-aware glue keeps those together while the paragraph stays nicely balanced.
CodePen: https://codepen.io/jlorenzetti/pen/zxvXewX

TL;DR

  • text-wrap: pretty improves line breaks, but it’s not language-aware.
  • This tiny polyfill adds a thin layer of editorial glue: keeps obvious pairs together (e.g. Mr. Smith, Fig. 2, 20 °C, 1900–2000, J. K. Rowling) and optionally keeps minor words attached to the next word where that’s the editorial norm.
  • Zero deps, one DOM pass, no layout measurements.
Try the live demo

What pretty solves — and what it can’t

text-wrap: pretty focuses on avoiding short last lines and can make smarter break choices near the end of a paragraph (details vary by engine).
But it’s agnostic about editorial semantics — it doesn’t know that Fig. and 2 belong together, or that many editorial traditions avoid breaking after short function words.

This polyfill adds just enough semantics on top of pretty, without re-implementing layout. Think of it as one layer of “do not break here” for well-established editorial conventions.

The upgrade in one minute

  • Scope: only elements whose computed text-wrap is pretty.
  • Scan: a single pass over text nodes (no layout reads, no reflows).
  • Decide: match language-aware patterns:
    • Safe rules (always on): honorifics + name, initials + surname, label + number, number + unit, § + number, numeric ranges (WORD JOINER around the dash).
    • Minor words (per locale): attach short function words only where this is a common editorial convention.
  • Glue: replace a normal space with NBSP (U+00A0) or insert WORD JOINER (U+2060) where appropriate.
  • Idempotent & safe: skips URLs/emails, doesn’t cross inline elements by default, doesn’t touch pre/code/....

Quick start

Plain HTML (no build)

<article class="typo" lang="en">
  <p>See Fig. 2 for details.</p>
</article>

<style>
  .typo { text-wrap: pretty; hyphens: auto; }
  /* Older UAs without `text-wrap: pretty`: opt-in to the glue layer */
  @supports not (text-wrap: pretty) {
    .typo { --text-wrap-preferences: minor-words; }
  }
</style>

<script type="module">
  import { init, registerLanguage } from 'https://cdn.jsdelivr.net/npm/text-wrap-minor-words@0.3.1/dist/lite.mjs';
  import en from 'https://cdn.jsdelivr.net/npm/text-wrap-minor-words@0.3.1/locales/en.json' assert { type: 'json' };

  registerLanguage('en', en);
  const ctrl = init({ languages: ['en'] });
  ctrl.process();
</script>

Swap en for another locale if needed. To support multiple locales, import/register more JSON files the same way.

Node / bundler (ESM)

npm i text-wrap-minor-words
import { init, registerLanguage } from 'text-wrap-minor-words/lite';
import en from 'text-wrap-minor-words/locales/en.json';

registerLanguage('en', en);
const ctrl = init({ languages: ['en'], observe: true });
ctrl.process();
<style>
  .typo { text-wrap: pretty; hyphens: auto; }
  @supports not (text-wrap: pretty) {
    .typo { --text-wrap-preferences: minor-words; }
  }
</style>

You can also load the non-lite global build:
<script src="https://cdn.jsdelivr.net/npm/text-wrap-minor-words@0.3.1/dist/index.global.js"></script> (includes built-in locale data for quick trials; prefer lite in production).

English display opt-in (optional)

By default, minor words are off for English body text. This snippet enables them only in display contexts (headings).

Plain HTML (no build)

<article class="typo" lang="en">
  <h1>From a book to the browser: a practical guide to typography on the web</h1>
</article>

<style>
  .typo { text-wrap: pretty; hyphens: auto; }

  /* Enable the minor-words glue only for English headings (display) */
  .typo :is(h1,h2,h3,h4,h5,h6):lang(en) {
    --text-wrap-preferences: minor-words; /* gate on */
    --text-wrap-minor-threshold: 1; /* glue after 1-char tokens */
    --text-wrap-minor-stoplist: "of to in on at for by a I"; /* adjust to your style guide */
  }

  @supports not (text-wrap: pretty) {
    .typo { --text-wrap-preferences: minor-words; }
  }
</style>

<script type="module">
  import { init, registerLanguage } from 'https://cdn.jsdelivr.net/npm/text-wrap-minor-words@0.3.1/dist/lite.mjs';
  import en from 'https://cdn.jsdelivr.net/npm/text-wrap-minor-words@0.3.1/locales/en.json' assert { type: 'json' };

  registerLanguage('en', en);
  const ctrl = init({ languages: ['en'], context: 'display' }); // limit processing to display contexts
  ctrl.process();
</script>

Bundler / ESM

npm i text-wrap-minor-words
import { init, registerLanguage } from 'text-wrap-minor-words/lite';
import en from 'text-wrap-minor-words/locales/en.json';

registerLanguage('en', en);
const ctrl = init({ languages: ['en'], context: 'display' });
ctrl.process();
.typo { text-wrap: pretty; hyphens: auto; }
.typo :is(h1,h2,h3,h4,h5,h6):lang(en) {
  --text-wrap-preferences: minor-words;
  --text-wrap-minor-threshold: 1;
  --text-wrap-minor-stoplist: "of to in on at for by a I";
}
@supports not (text-wrap: pretty) {
  .typo { --text-wrap-preferences: minor-words; }
}

Tip: prefer :lang(en) over [lang="en"] so headings match the language inherited from the container.

Defaults & language notes (compact)

  • Safe rules (on in every language): honorifics + name (Mr. Smith, Dr. Müller), initials + surname (J. K. Rowling), label + number (Fig. 2, S. 12), number + unit (20 °C, 10 km), § + number, numeric ranges (1900–2000 with U+2060 around the dash).
  • Minor words: on by default in Romance/Slavic/Greek; off by default in English/German/Dutch (you can opt-in for display).
Locale 1-letter glue Examples from the default stop-list*
fr de, du, le, la, les, un, une…
it di, da, in, su, con, per…
pl w, z, do, na, po…
ru в, к, с, на, по…
el σε, το, τη, οι…
en (safe rules by default; opt-in for display)

* Full lists and configuration in the language dataset.

Performance & accessibility

  • O(n) over text nodes. Regex pre-compiled per locale. No layout measurements.
  • Early exit for neutral locales (e.g. en/de/nl when minor words are off).
  • Screen readers: NBSP/WORD JOINER are invisible to AT; URL/email detection prevents accidental glue.

Caveats

  • Inline crossing: currently we don’t cross inline elements (e.g. a <em>word</em>). We may evaluate a future opt-in.
  • CJK/RTL: not targeted for now (different line-breaking traditions).
  • Not a grammar rule: this encodes editorial conventions, not linguistic rules. Defaults are conservative; override when your style guide differs.

Spec angle (why this might belong in CSS)

This polyfill explores a small, language-aware extension to text-wrap. If real-world use proves it broadly useful and low-risk, one possible shape could be a dedicated option (e.g. a “minor-words” sub-mode) that lets UAs apply these low-controversy “do not break here” hints.

The repo includes an explainer with non-goals, locale data, and tests. If you have production samples or edge cases, please share them — they’re exactly what a future proposal needs.

Try it & tell me what breaks

Got an edge case (especially in English display, or a new locale)? Open an issue or drop a comment — examples and screenshots are gold.


This content originally appeared on DEV Community and was authored by Jacopo Lorenzetti


Print Share Comment Cite Upload Translate Updates
APA

Jacopo Lorenzetti | Sciencx (2025-09-17T14:35:00+00:00) Beyond `text-wrap: pretty` — language-aware line breaks for minor words. Retrieved from https://www.scien.cx/2025/09/17/beyond-text-wrap-pretty-language-aware-line-breaks-for-minor-words/

MLA
" » Beyond `text-wrap: pretty` — language-aware line breaks for minor words." Jacopo Lorenzetti | Sciencx - Wednesday September 17, 2025, https://www.scien.cx/2025/09/17/beyond-text-wrap-pretty-language-aware-line-breaks-for-minor-words/
HARVARD
Jacopo Lorenzetti | Sciencx Wednesday September 17, 2025 » Beyond `text-wrap: pretty` — language-aware line breaks for minor words., viewed ,<https://www.scien.cx/2025/09/17/beyond-text-wrap-pretty-language-aware-line-breaks-for-minor-words/>
VANCOUVER
Jacopo Lorenzetti | Sciencx - » Beyond `text-wrap: pretty` — language-aware line breaks for minor words. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/09/17/beyond-text-wrap-pretty-language-aware-line-breaks-for-minor-words/
CHICAGO
" » Beyond `text-wrap: pretty` — language-aware line breaks for minor words." Jacopo Lorenzetti | Sciencx - Accessed . https://www.scien.cx/2025/09/17/beyond-text-wrap-pretty-language-aware-line-breaks-for-minor-words/
IEEE
" » Beyond `text-wrap: pretty` — language-aware line breaks for minor words." Jacopo Lorenzetti | Sciencx [Online]. Available: https://www.scien.cx/2025/09/17/beyond-text-wrap-pretty-language-aware-line-breaks-for-minor-words/. [Accessed: ]
rf:citation
» Beyond `text-wrap: pretty` — language-aware line breaks for minor words | Jacopo Lorenzetti | Sciencx | https://www.scien.cx/2025/09/17/beyond-text-wrap-pretty-language-aware-line-breaks-for-minor-words/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.