CSS hyphens: auto, and why it needs a lang attribute

hyphens: auto lets the browser split long words at syllables and add the hyphen itself. It only works when the text has a language set with lang.

To let the browser split long words at syllables, add two things: hyphens: auto in the CSS and a lang attribute in the HTML. The second one is the part people miss. Without a language, hyphens: auto quietly does nothing.

<html lang="en">
...
<style>
  p { hyphens: auto; }
</style>

Try it below. Switch between the three values, untick the lang box, and drag the column narrower.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<!-- no lang on <html> on purpose: the checkbox adds it to the paragraph -->
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>hyphens: auto</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .controls { display: grid; gap: 10px; margin-bottom: 14px; font-size: 14px; }
  .controls label { display: flex; align-items: center; gap: 8px; }
  .seg button {
    font: 600 13px ui-monospace, Consolas, monospace; padding: 6px 10px;
    border: 1px solid #c9ced6; background: #fff; border-radius: 6px; cursor: pointer;
  }
  .seg button.on { background: #1d4ed8; border-color: #1d4ed8; color: #fff; }
  code, pre { font: 13px ui-monospace, Consolas, monospace; }
  pre { margin: 0 0 12px; padding: 8px 10px; background: #1d2330; color: #e8ebf0; border-radius: 8px; white-space: pre-wrap; }
  .col {
    width: 170px; max-width: 100%; padding: 10px 12px; box-sizing: border-box;
    background: #fff; border: 2px dashed #9aa3b2; border-radius: 10px;
    font-size: 17px; line-height: 1.45;
  }
  .col p { margin: 0; }
</style>
</head>
<body>
<div class="controls">
  <div class="seg" id="seg">
    <button data-v="none">none</button>
    <button data-v="manual" class="on">manual</button>
    <button data-v="auto">auto</button>
  </div>
  <label><input type="checkbox" id="lang" checked> <code>lang="en"</code> on the paragraph</label>
  <label>Column width <input type="range" id="w" min="140" max="320" value="170"> <span id="wOut">170px</span></label>
</div>
<pre id="code"></pre>
<div class="col" id="col">
  <p id="p" lang="en">The committee published comprehensive recommendations on accessibility, interoperability and internationalization.</p>
</div>

<script>
  const p = document.getElementById('p');
  const col = document.getElementById('col');
  const lang = document.getElementById('lang');
  let value = 'manual';  // the default: break only at "-" and &shy;

  function render() {
    p.style.hyphens = value;
    if (lang.checked) p.setAttribute('lang', 'en'); else p.removeAttribute('lang');
    document.getElementById('code').textContent =
      (lang.checked ? '<p lang="en">' : '<p>') + '\np { hyphens: ' + value + '; }';
  }

  document.getElementById('seg').addEventListener('click', (e) => {
    if (!e.target.dataset.v) return;
    value = e.target.dataset.v;
    document.querySelectorAll('#seg button').forEach((b) => b.classList.toggle('on', b === e.target));
    render();
  });
  lang.addEventListener('change', render);
  document.getElementById('w').addEventListener('input', (e) => {
    col.style.width = e.target.value + 'px';
    document.getElementById('wOut').textContent = e.target.value + 'px';
  });
  render();
</script>
</body>
</html>
Switch none, manual and auto, and remove lang to see auto stop working.

At a narrow width, manual and none leave long words sticking out of the column. auto with a language splits them and adds the hyphen at the line end. Remove lang and auto behaves exactly like manual again.

The three values: none, manual, auto

hyphens decides where a line may break inside a word. Spaces are always allowed as break points; this property is only about the inside of words.

Same text, same width: none ignores the soft hyphen, manual uses it, auto adds its own breaks.
Same text, same width: none ignores the soft hyphen, manual uses it, auto adds its own breaks.
Value Breaks at &shy; Adds its own breaks Needs lang
none No No No
manual (default) Yes No No
auto Yes Yes, from a dictionary Yes

With every value, a line can still break right after a hyphen that is typed in the text, such as "well-known". hyphens: none only switches off the soft hyphens and the automatic breaks.

The property is inherited, so one rule on body or article covers every paragraph inside.

Why hyphens: auto needs lang

Hyphenation rules differ by language. English, German and Dutch split words in different places. The browser picks its rules from the language of the element, and that comes from the lang attribute on the element or on any ancestor.

The same CSS with and without a language. Only the right one splits the long word.
The same CSS with and without a language. Only the right one splits the long word.

Three things to check when auto does not split anything:

  1. Is lang set? Put it on <html> once. A lang on a wrapping div also works.

  2. Is the code real? A made-up code such as lang="zz" gives the browser no rules, and nothing splits.

  3. Does the browser have rules for that language? Hyphenation dictionaries come with the browser or the operating system. If one is missing for a language, auto leaves that text alone.

A mixed-language page can set lang per element, such as <blockquote lang="de">, and each part gets its own rules.

Soft hyphens for words you choose

A soft hyphen, written &shy; in HTML, marks one spot where a word may break. While the word fits, it is invisible. When the line has to break inside that word, the browser breaks there and shows a hyphen.

<div class="tile">Photo&shy;synthesis</div>

Soft hyphens work with the default hyphens: manual, and they do not need a lang attribute. That makes them the right tool for a few known words: product names, long headings, labels in narrow cards or grid tiles.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Soft hyphens and hyphenate-character</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .controls { display: grid; gap: 10px; margin-bottom: 14px; font-size: 14px; }
  .controls label { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
  code, select { font: 13px ui-monospace, Consolas, monospace; }
  .tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; width: 330px; max-width: 100%; }
  .tile {
    min-width: 0; padding: 12px 8px; border-radius: 10px; background: #fff;
    border: 1px solid #dfe3e8; text-align: center;
    font-weight: 700; font-size: 17px; line-height: 1.3;
    overflow: hidden;
  }
  .tile small { display: block; margin-top: 6px; font-weight: 400; font-size: 12px; color: #6b7280; }
  /* the two properties this example changes */
  .tile { hyphens: manual; hyphenate-character: auto; }
</style>
</head>
<body>
<div class="controls">
  <label><code>hyphens:</code>
    <select id="hy"><option>manual</option><option>none</option></select>
  </label>
  <label><code>hyphenate-character:</code>
    <select id="hc">
      <option value="auto">auto</option>
      <option value='"="'>"="</option>
      <option value='"\2192"'>"&rarr;"</option>
      <option value='""'>"" (no mark)</option>
    </select>
  </label>
  <label>Grid width <input type="range" id="w" min="300" max="540" value="330"></label>
</div>
<div class="tiles" id="tiles">
  <!-- &shy; marks where a word may break. It stays invisible until it is used. -->
  <div class="tile">Photo&shy;synthesis<small>Biology</small></div>
  <div class="tile">Thermo&shy;dynamics<small>Physics</small></div>
  <div class="tile">Micro&shy;economics<small>Economics</small></div>
</div>

<script>
  const tiles = document.querySelectorAll('.tile');
  function set(prop, v) { tiles.forEach((t) => t.style.setProperty(prop, v)); }

  document.getElementById('hy').addEventListener('change', (e) => set('hyphens', e.target.value));
  document.getElementById('hc').addEventListener('change', (e) => set('hyphenate-character', e.target.value));
  document.getElementById('w').addEventListener('input', (e) => {
    document.getElementById('tiles').style.width = e.target.value + 'px';
  });
</script>
</body>
</html>
Three tiles with one soft hyphen each. Try none, change the hyphen mark, and widen the grid.

Set the tiles to none and the words stop breaking. They get clipped at the tile edge instead. Widen the grid and the words fit on one line, so the soft hyphens stay hidden.

hyphenate-character: change or hide the hyphen

hyphenate-character sets the text shown at the end of a line where a word was split. The default, auto, picks the usual hyphen for the language.

.tile { hyphenate-character: "="; }   /* any short string */
.tile { hyphenate-character: ""; }    /* break with no mark */

It applies to breaks at &shy; and to breaks made by hyphens: auto. It does not change a hyphen that is typed in the text. An empty string is handy for tight UI labels where a dangling hyphen looks like a mistake.

text-align: justify with hyphens

Justified text stretches the spaces on each line so both edges are straight. In a narrow column, a long word that cannot break must move down whole. The line it left behind is short, and the browser widens its spaces to fill it.

Justified text without and with hyphenation, at the same width.
Justified text without and with hyphenation, at the same width.

hyphens: auto shrinks those gaps, because the long word can split and fill the line. The next example measures the widest gap between two words, in pixels, as you switch things on.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Justified column with hyphens</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .controls { display: grid; gap: 8px; margin-bottom: 12px; font-size: 14px; }
  .controls label { display: flex; align-items: center; gap: 8px; }
  code { font: 13px ui-monospace, Consolas, monospace; background: #e8ebf0; padding: 1px 4px; border-radius: 4px; }
  .meter { font-size: 14px; margin-bottom: 10px; }
  .meter b { font: 700 15px ui-monospace, Consolas, monospace; }
  article {
    width: 240px; max-width: 100%; box-sizing: border-box; padding: 14px 16px;
    background: #fff; border-radius: 10px; box-shadow: 0 2px 10px rgba(0, 0, 0, .08);
    font: 16px/1.5 Georgia, serif;
    text-align: justify;   /* straight left and right edges */
  }
  article p { margin: 0 0 10px; }
  article p:last-child { margin: 0; }
  article.hy { hyphens: auto; }              /* works because <html lang="en"> */
  article.last { text-align-last: justify; } /* also stretch each last line */
</style>
</head>
<body>
<div class="controls">
  <label><input type="checkbox" id="hy"> <code>hyphens: auto</code></label>
  <label><input type="checkbox" id="last"> <code>text-align-last: justify</code></label>
  <label><code>word-spacing</code> <input type="range" id="ws" min="0" max="8" value="0"> <span id="wsOut">0px</span></label>
</div>
<div class="meter">Widest gap between two words: <b id="gap">-</b></div>
<article id="art">
  <p>Narrow justified columns often show uncomfortable gaps, because unbreakable words like responsibilities, internationalization or documentation must move down whole and leave stretched spaces behind.</p>
  <p>With hyphenation on, the browser can split those words at syllables, so each line fills closer to the edge.</p>
</article>

<script>
  const art = document.getElementById('art');

  // Measure every space character and show the widest one.
  function widestGap() {
    let max = 0;
    const walk = document.createTreeWalker(art, NodeFilter.SHOW_TEXT);
    const r = document.createRange();
    while (walk.nextNode()) {
      const t = walk.currentNode;
      for (let i = 0; i < t.length; i++) {
        if (t.data[i] !== ' ') continue;
        r.setStart(t, i); r.setEnd(t, i + 1);
        max = Math.max(max, r.getBoundingClientRect().width);
      }
    }
    document.getElementById('gap').textContent = Math.round(max) + 'px';
  }

  document.getElementById('hy').addEventListener('change', (e) => { art.classList.toggle('hy', e.target.checked); widestGap(); });
  document.getElementById('last').addEventListener('change', (e) => { art.classList.toggle('last', e.target.checked); widestGap(); });
  document.getElementById('ws').addEventListener('input', (e) => {
    art.style.wordSpacing = e.target.value + 'px';
    document.getElementById('wsOut').textContent = e.target.value + 'px';
    widestGap();
  });
  widestGap();
</script>
</body>
</html>
Turn on hyphens: auto and watch the widest gap shrink. Then try text-align-last and word-spacing.

In this 240px column, the widest gap dropped from 52px to 22px when hyphens: auto was switched on (measured in Chromium on Windows; other fonts give other numbers).

Two related properties show up in the example:

  • text-align-last sets the alignment of the last line of a paragraph. With plain justify, the last line stays at the start. text-align-last: justify stretches it too, which can leave very wide gaps on a short last line. Leave it off for body text.

  • word-spacing adds a fixed amount to every space. In justified text the browser still stretches on top of it, so it raises the minimum gap, not the maximum.

For left-aligned text in a normal-width column, hyphenation is optional. Its main use is narrow columns, phones, and justified text. text-align covers the other alignment values.

hyphens vs other ways to break long text

hyphens breaks words at syllables and marks the break. Other properties break text in different ways, and they are easy to mix up.

Property What it does to a long word
hyphens: auto Splits at a syllable and adds a hyphen
overflow-wrap: anywhere Breaks anywhere, only if the word would overflow, no hyphen
word-break: break-all Breaks between any two letters, no hyphen
white-space: nowrap Stops wrapping altogether

A URL or a long code string has no syllables, so hyphens: auto will not split it. Use overflow-wrap for those; breaking long words goes through the options. If you want the opposite, text that stays on one line, see white-space and nowrap.

When it does not work

What you see Cause Fix
hyphens: auto changes nothing No lang on the element or any ancestor Add lang="en" to <html>
Works in one language, not another No hyphenation dictionary for that language in this browser Mark key words with &shy;
A capitalized word is never split In Chromium and Firefox tests, a word starting with a capital letter was not split by auto Add &shy; to that word
A URL still overflows URLs have no syllables to split overflow-wrap: anywhere
"well-known" breaks even with none A typed hyphen is always a break point Use &#8209; (non-breaking hyphen)
Huge gaps on the last line text-align-last: justify Remove it, keep text-align: justify
&shy; shows up as text It was escaped as &amp;shy; Write &shy; in the HTML source

Hyphenation depends on the width of the column, so a screenshot shows only one case. To let someone drag the width and see the words split, send the working page.

Paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can try each value themselves. If you change the code later, the same link shows the new version.

Questions people ask

Why does hyphens: auto do nothing?

The text has no language. Add lang="en" (or the right language code) to the html element or to the element that holds the text. Without a language the browser has no hyphenation rules to look up, so it treats auto like manual.

What is the default value of hyphens?

manual. The browser breaks a word only where the text already offers a break: after a typed hyphen, or at a soft hyphen (&shy;). It never invents break points of its own.

What is &shy; in HTML?

A soft hyphen (U+00AD). It is invisible while the word fits on the line. If the line has to break inside that word, the browser breaks there and shows a hyphen at the end of the line. It works without a lang attribute.

Does hyphens: none stop all breaks inside words?

No. It ignores soft hyphens and never adds its own, but a line can still break after a hyphen that is typed in the text, such as the one in "well-known". To keep such a word together, use a non-breaking hyphen (&#8209;) or white-space: nowrap on that word.

Can I change the hyphen character?

Yes, with hyphenate-character. It takes a string, such as hyphenate-character: "=", and an empty string "" breaks the word with no mark at all. The default value, auto, uses the right character for the text's language.

Keep reading