DEV Community

Jacopo Lorenzetti
Jacopo Lorenzetti

Posted on

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, 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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
<style>
  .typo { text-wrap: pretty; hyphens: auto; }
  @supports not (text-wrap: pretty) {
    .typo { --text-wrap-preferences: minor-words; }
  }
</style>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Bundler / ESM

npm i text-wrap-minor-words
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
.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; }
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (1)

Collapse
 
jlorenzetti profile image
Jacopo Lorenzetti

If you try this, I'd love your feedback and edge cases — especially for English display and new locales. Screenshots welcome!

Live demo → jlorenzetti.github.io/text-wrap-mi...