The Long URL That Breaks Your Table: Tailwind v4's wrap-anywhere vs wrap-break-word

Tailwind wrap-anywhere fixes long URLs that wrap-break-word cannot, because only anywhere counts mid-word breaks toward min-content size. Full utility map.

Steven Richardson
Steven Richardson
· 11 min read

A user pastes a 180-character tracking URL into a comment field and the whole page gains a horizontal scrollbar. You reach for break-words, which does nothing. You try break-all, which fixes the URL and turns every other sentence in the component into a ransom note. Someone eventually suggests min-w-0 from a Stack Overflow answer and it works, but nobody can explain why.

Tailwind v4.1 added wrap-anywhere to fix this properly. The release notes explain it in two sentences, which is not enough to know when to reach for it — so here is the CSS underneath, and the one sizing rule that makes the whole thing make sense.

Three layouts, one long URL, three different failures#

The same string fails differently depending on the formatting context it lands in.

<!-- 1. Auto-layout table: the column stretches, the table overflows -->
<table class="w-full">
  <tr><td>https://example.com/r?utm_source=...&utm_campaign=...</td></tr>
</table>

<!-- 2. Flex row: the text block refuses to shrink, the avatar gets squashed -->
<div class="flex max-w-sm gap-3">
  <img class="size-10 shrink-0 rounded-full" src="/avatar.jpg" alt="">
  <p>https://example.com/r?utm_source=...&utm_campaign=...</p>
</div>

<!-- 3. Grid card with an arbitrary template: the column blows past 1fr -->
<div class="grid grid-cols-[1fr_auto] gap-4">
  <p>https://example.com/r?utm_source=...&utm_campaign=...</p>
  <span>12:04</span>
</div>

Three different containers, one cause: every one of them sizes a box from its min-content width, and the min-content width of an unbreakable 180-character string is 180 characters. The fix is not "make the text wrap" — the text was always allowed to wrap. The fix is to change what the browser thinks the minimum is.

overflow-wrap and word-break are not the same property#

This is the part that makes everything else obvious, and the naming actively works against you.

overflow-wrap answers: may the browser break inside a word, but only when that word would otherwise overflow? Values: normal | break-word | anywhere. It is a last resort — MDN is explicit that it "will only create a break if an entire word cannot be placed on its own line without overflowing."

word-break answers: how should the browser break words in general? Values: normal | break-all | keep-all. This one is not a last resort. break-all breaks every word at any character, all the time.

Tailwind maps wrap-* to the first and break-* to the second. Two namespaces, two properties, and one legacy class that sat in the wrong namespace for years.

The full Tailwind overflow-wrap and word-break utility map#

Class CSS Long URL Ordinary prose Inside a flex item
wrap-normal overflow-wrap: normal overflows untouched overflows
wrap-break-word overflow-wrap: break-word wraps in block flow untouched still overflows
wrap-anywhere overflow-wrap: anywhere wraps untouched wraps
break-normal word-break: normal overflows untouched overflows
break-all word-break: break-all wraps mangled wraps
break-keep word-break: keep-all overflows untouched (CJK only) overflows
truncate overflow: hidden + text-overflow: ellipsis + white-space: nowrap one line, ellipsis one line, ellipsis needs min-w-0
line-clamp-3 -webkit-line-clamp: 3 three lines, ellipsis three lines, ellipsis needs a width
hyphens-auto hyphens: auto no effect hyphenates no effect

Note the column that matters: break-all is the only one that damages ordinary prose. In a table cell holding both a URL and a sentence, it wrecks the sentence. Keep it for CJK text and opaque tokens like hashes.

What happened to break-words

break-words is gone from the word-break docs, and it was always the odd one out: it lived in the break-* namespace but emitted overflow-wrap: break-word, not a word-break value. That mismatch is the root of years of confusion.

It is deprecated, not removed — existing markup keeps working. As of v4.1.15 the codemod rewrites it for you, so if you are already running the official Tailwind v3 to v4 upgrade tool the migration is a no-op on your side:

npx @tailwindcss/upgrade

wrap-break-word is the same declaration under an honest name. Search-and-replace is safe.

Why wrap-break-word fails inside a flex item and Tailwind wrap-anywhere does not#

Here is the whole thing, straight from the spec text for each value.

overflow-wrap: break-word — words may be broken at arbitrary points, "but soft wrap opportunities introduced by the word break are NOT considered when calculating min-content intrinsic sizes."

overflow-wrap: anywhere — same breaking behaviour, except "soft wrap opportunities introduced by the word break are considered when calculating min-content intrinsic sizes."

That single clause is the entire difference. A flex item defaults to min-width: auto, which resolves to its min-content width and refuses to shrink below it. With break-word, min-content is still the full unbroken URL, so the item is sized as though the whole string must fit on one line — and it overflows before the break rule ever gets a chance to apply. With anywhere, min-content collapses to roughly one character, so the item shrinks and the text wraps.

<!-- Broken: min-content is still the full URL -->
<div class="flex max-w-sm gap-3">
  <img class="size-10 shrink-0 rounded-full" src="/avatar.jpg" alt="">
  <div class="wrap-break-word">
    <p class="font-medium">Jay Riemenschneider</p>
    <p>https://example.com/r?utm_source=newsletter&utm_campaign=q3</p>
  </div>
</div>

<!-- Fixed: min-content collapses, the item shrinks -->
<div class="flex max-w-sm gap-3">
  <img class="size-10 shrink-0 rounded-full" src="/avatar.jpg" alt="">
  <div class="wrap-anywhere">
    <p class="font-medium">Jay Riemenschneider</p>
    <p>https://example.com/r?utm_source=newsletter&utm_campaign=q3</p>
  </div>
</div>

Check it yourself rather than taking my word for it: select the wrapping div in devtools and set its width to min-content. With wrap-break-word the box stays as wide as the URL; with wrap-anywhere it collapses to a single column of characters. Same markup, same font, one declaration apart.

This is the second flex-sizing trap in Tailwind that comes down to intrinsic sizing rather than the utility you applied — the other being flex and grid items getting cut off by centred alignment, which has the same "the class is fine, the box model is not" shape.

Where min-w-0 is still required alongside wrap-anywhere#

wrap-anywhere changes what min-content measures to. It does not change the fact that flex items have min-width: auto. So it resolves the case where the min-content size is text, and does nothing when the min-content size comes from something else.

The common failure is a truncating descendant. truncate includes white-space: nowrap, and a nowrap child has a min-content width equal to its entire unbroken line — text wrapping rules do not apply to it at all:

<!-- wrap-anywhere alone is not enough here -->
<div class="flex gap-3">
  <div class="wrap-anywhere min-w-0 flex-1">
    <p class="truncate font-medium">A very long display name that must stay on one line</p>
    <p>https://example.com/r?utm_source=newsletter&utm_campaign=q3</p>
  </div>
  <span class="shrink-0 text-sm">12:04</span>
</div>

The rule I use: if every descendant wraps, wrap-anywhere is sufficient. If any descendant is truncate, whitespace-nowrap, an image, a <pre>, or a nested table, you still need min-w-0 on the flex item.

In grid, the equivalent is minmax(0, 1fr). Tailwind's grid-cols-* utilities already generate repeat(N, minmax(0, 1fr)), so standard grids are immune — but an arbitrary template like grid-cols-[1fr_auto] uses a raw 1fr, whose automatic minimum is min-content, and the overflow is back. Write grid-cols-[minmax(0,1fr)_auto] instead. Worth knowing before you start aligning card grids to a parent grid, where nested tracks multiply the problem.

Hyphenation, truncation, and picking per context#

For prose in a narrow column, hyphenation reads better than a hard mid-word break. hyphens-auto is dictionary-driven, so it does nothing at all without a lang attribute on an ancestor — a five-second fix in a Blade layout that people spend an afternoon on:

{{-- resources/views/components/layouts/app.blade.php --}}
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<p class="hyphens-auto text-pretty">Kraftfahrzeughaftpflichtversicherung...</p>

Truncation is the other answer to the same complaint: hide the overflow instead of wrapping it. truncate for one line, line-clamp-3 for several — both are core in v4, no plugin. Truncated text needs the full value recoverable, so pair it with title or a tooltip.

My default by context: table cells truncate, chat bubbles and comment bodies wrap, code blocks scroll. A data table is usually fixed the layer below anyway — table-fixed w-full with explicit column widths stops the column stretching in the first place, and then the wrap utility only has to handle what is inside it:

<table class="w-full table-fixed">
  <colgroup>
    <col class="w-1/3"><col class="w-2/3">
  </colgroup>
  <tbody>
    <tr>
      <td class="truncate" title="https://example.com/r?utm_source=...">https://example.com/r?utm_source=...</td>
      <td class="wrap-anywhere">Notes from the user, which should wrap normally.</td>
    </tr>
  </tbody>
</table>

Without table-fixed the browser sizes columns from content, and no amount of wrapping in one cell stops another column winning the fight. If you are styling those rows, striped table rows with nth-* variants compose with all of this cleanly.

A Blade component for untrusted user text#

Sprinkling wrap-anywhere across forty templates is how this regresses. User-generated content is where it always bites — comment bodies, pasted spreadsheet cells, imported product descriptions — so name the concept once:

{{-- resources/views/components/user-text.blade.php --}}
@props(['clamp' => null])

@php
    // Literal class strings only. Tailwind's scanner cannot see a class
    // assembled by string concatenation at render time.
    $clampClass = match ((int) $clamp) {
        2 => 'line-clamp-2',
        3 => 'line-clamp-3',
        default => null,
    };
@endphp

<div {{ $attributes->class(array_filter(['wrap-anywhere', 'min-w-0', $clampClass])) }}>
    {{ $slot }}
</div>
<x-user-text :clamp="3" class="text-sm text-zinc-600">
    {{ $comment->body }}
</x-user-text>

The match is deliberate. Building 'line-clamp-'.$clamp produces a class Tailwind never sees in your source files and never generates — if you do need dynamic values there, safelist them explicitly with @source inline rather than hoping the scanner catches them. The same instinct applies to inputs: auto-growing textareas with field-sizing solve the write side of the same user-content problem.

Gotchas and Edge Cases#

truncate cancels wrap-anywhere. truncate sets white-space: nowrap, and nowrap wins — no wrapping of any kind happens. If "the fix did not work", check for truncate or a stray whitespace-nowrap on the element or an ancestor before changing anything else.

overflow-wrap inherits, min-width does not. Putting wrap-anywhere on a parent covers descendant text, which is why the component above works. But every flex item in the subtree still has its own min-width: auto to deal with.

Breaking a URL is not the same as hyphenating one. overflow-wrap inserts no characters, so a broken URL still copies cleanly. CSS hyphenation inserts a visible hyphen — never put hyphens-auto on a URL you expect people to copy.

Browser support is a non-issue. overflow-wrap has been Baseline widely available since October 2018, and Tailwind v4 targets Safari 16.4+ anyway. word-wrap is a legacy alias of the same property; you will still see it in old stylesheets.

Don't stack wrap-anywhere and break-all. They set different properties, so both apply, and break-all wins for ordinary prose — you get the mangled-sentence behaviour you were trying to avoid.

Wrapping Up#

Reach for wrap-anywhere whenever the text is user-supplied and the container is flex; wrap-break-word is fine for plain block flow. Keep min-w-0 for flex items with a truncating or non-text child, use minmax(0,1fr) in arbitrary grid templates, and fix data tables with table-fixed before reaching for a wrap utility at all.

If the overflow you are chasing turns out to be a sizing problem rather than a text problem, Tailwind v4 container queries are usually the better tool. For data tables specifically, server-driven pagination and sorting with Flux UI Pro tables covers the layer where column widths actually get decided.

FAQ#

What is the difference between wrap-anywhere and wrap-break-word in Tailwind?

Both allow the browser to break inside a word to prevent overflow, and both only do so as a last resort. The difference is intrinsic sizing: wrap-anywhere counts those mid-word break opportunities when calculating the element's min-content width, and wrap-break-word does not. That makes wrap-anywhere the one that works inside flex containers, where the item's minimum size is derived from min-content.

How do I stop a long URL breaking my layout in Tailwind?

Add wrap-anywhere to the element containing the URL. If it sits in a flex row, that is usually the complete fix. If it sits in a table, add table-fixed w-full to the table and give the columns explicit widths first, because an auto-layout table sizes columns from content and will stretch regardless of what the cell does. Check for a truncate or whitespace-nowrap class first — either one prevents wrapping entirely.

What is the difference between break-words and break-all?

They are not even the same CSS property. break-words was Tailwind's old class for overflow-wrap: break-word, which breaks a word only when it would otherwise overflow the line. break-all maps to word-break: break-all, which breaks every word at any character all the time, so normal sentences come out fragmented. Use break-all only for CJK text or opaque tokens like hashes and IDs. break-words is deprecated in favour of wrap-break-word.

Why does break-words not work inside a flex container?

Because overflow-wrap: break-word does not affect the element's min-content intrinsic size. A flex item has min-width: auto by default, so it refuses to shrink below its min-content width, and with break-word that width is still the entire unbroken word. The item is laid out at full width and overflows before the break rule can apply. wrap-anywhere fixes it by making those break points count toward min-content, and min-w-0 fixes it by removing the minimum altogether.

Do I still need min-w-0 with wrap-anywhere?

Often not, but not never. wrap-anywhere resolves the case where the flex item's minimum size comes from text. It does nothing when that minimum comes from something else — a truncate or whitespace-nowrap descendant, an image, a <pre>, or a nested table. The practical rule is that if every descendant wraps, wrap-anywhere alone is enough; if any descendant cannot wrap, keep min-w-0 on the flex item as well.

Should I truncate or wrap long text in a table cell?

Truncate, in almost every case. A table row is a scannable unit and a wrapped 180-character URL destroys the row rhythm for every other row on screen. Use truncate with a title attribute so the full value is still recoverable, and set table-fixed with explicit column widths so the truncation happens at a predictable point. Wrapping belongs in contexts where the full text is the content — comment bodies, chat messages, descriptions.

Steven Richardson
Steven Richardson

CTO at Digitonic. Writing about Laravel, architecture, and the craft of leading software teams from the west coast of Scotland.