Every post with a code block looked broken on my phone. Not the code block. The paragraphs. Ordinary sentences ran past the right edge of the screen and got cut off mid-word, and the whole page could be dragged sideways into a strip of nothing. On a laptop it was perfect. I had overflow-x: clip on the root element for exactly this situation, and it made no difference I could see. It took me a while to find the actual cause, and it was not where I was looking.

The usual suspect

A code block is the obvious culprit for sideways overflow. Long lines of code don’t wrap, so a wide pre pushes its parent open. Except mine was already handled. Tailwind’s typography plugin gives .prose pre an overflow-x: auto, so a long line scrolls inside the block instead of pushing the page.

So the code block was behaving. The paragraphs weren’t. That was the real tell, and I ignored it for too long.

Clip hides. It doesn’t size.

Here is what overflow-x: clip does: it cuts off anything that pokes outside the box, without turning the box into a scroll container. Put it on html and the CSS spec says it propagates to the viewport, where clip “must be interpreted as hidden”. I checked in Chrome: with clip on the root, a sideways wheel swipe moves the page zero pixels. It works.

It just doesn’t do what I needed. Clipping decides what you can see. It has no say in where text wraps. My paragraphs were wrapping against a container wider than the screen, and clipping the overflow only guaranteed I could never scroll over to read the ends of the lines.

Two lines in DevTools told the whole story:

js
document.documentElement.clientWidth // 430
document.querySelector("main").getBoundingClientRect().width // 672

The main column was 672 pixels wide on a 430-pixel screen.

672 is not a random number

max-w-2xl in Tailwind is 42rem, which is 672px. That is the maximum width I gave the column, for desktop. On a phone, main should never get near it. It was sitting exactly at the ceiling, which meant the viewport wasn’t sizing it at all. Something was letting it grow from its content, and only the max-width was stopping it.

What’s actually happening

The layout is a common one: a flex column so the footer sits at the bottom, and a centred column of prose inside it.

html
<body class="flex min-h-screen flex-col">
  <main class="mx-auto max-w-2xl px-6">
    <p>Ordinary prose that should wrap at the screen edge.</p>
    <pre>const aVeryLongLineThatNeverWraps = "..."</pre>
  </main>
  <footer>...</footer>
</body>

In plain CSS, the part that matters:

css
body {
  display: flex;
  flex-direction: column;
}
main {
  margin-inline: auto; /* mx-auto */
  max-width: 42rem; /* max-w-2xl, 672px */
}

main is a flex item. In a column, the cross axis is horizontal, and a flex item’s default is to stretch across it, which is why you normally never think about the width of a flex column’s children. But the spec attaches a condition to that stretch, in one flat sentence: “If either of the flex item’s cross-axis margins are auto, align-self has no effect.”

mx-auto is two auto margins on the cross axis. That one class quietly opts main out of stretching. Without stretch, the spec falls back to the item’s hypothetical cross size, computed “treating auto as fit-content”.

Fit-content means: ask the content how wide it wants to be. Paragraphs are agreeable; they wrap to any width. A pre is not. It never wraps, so the narrowest it will ever agree to is its longest line. The overflow-x: auto on it doesn’t change that. A scroll container still reports its full content width when its parent asks how big it would like to be; scrolling only kicks in after the size is decided. So main asked for the width of my longest line of code, and max-width clamped that to 672.

Every paragraph inside then wrapped at 672px. The screen was 430px. The last 242px of every line were off-screen.

I measured the variations in a headless browser at a 430px viewport, with a long-line pre inside main:

main styles width
mx-auto max-w-2xl (as shipped) 672px
plus min-width: 0 672px
plus width: 100% 430px
auto margins removed 430px

The min-width: 0 row deserves a note. It is the famous flexbox fix, and it did nothing here. It fixes the automatic minimum size along the flex container’s main axis, which in a column is the vertical one. The horizontal problem was never a minimum. It was fit-content choosing a size, and fit-content doesn’t consult min-width.

The fix

Give main a definite width, so there is no fit-content step to run.

css
main {
  width: 100%;
}

A percentage of the body is a definite size. max-width still caps it at 672px on a laptop, and the auto margins still centre it there. On a phone it is 430px, and paragraphs wrap where the screen ends.

I shipped it with min-width: 0 alongside, out of habit. As the table shows, that line did nothing. Keep it or drop it, but know that width: 100% is the fix.

Two smaller things went in with it. The pre got max-width: 100%, a belt for the braces. And the viewport meta tag got initial-scale=1, which it had been missing. Safari’s default for a page wider than the screen is to zoom out until it fits, so for weeks the bug had disguised itself as “the text looks a bit small on the phone”. Adding initial-scale=1 fixes nothing. It stops the browser from hiding the problem, which is how I finally saw it.

The rule

When a page scrolls sideways, don’t reach for overflow-x: clip first. It hides overflow; it never fixes a box that is the wrong size. Find the box:

js
;[...document.querySelectorAll("*")].filter(
  (el) =>
    el.getBoundingClientRect().right > document.documentElement.clientWidth
)

Then ask why it is that wide. If it is a child of a flex column with mx-auto on it, you have your answer: auto margins turn stretch off, and fit-content will happily size your layout to your longest line of code. Give it width: 100%.