Tailwind CSS mx-auto on flex item causes unexpected horizontal overflow in SvelteKit mobile view
mx-auto on a flex item of a column flex container silently expands the mobile layout viewport, so the whole page renders wider than the device.
A SvelteKit + Tailwind app shell wraps every page in a column flex container:
<main class="container mx-auto flex flex-1 flex-col px-4 py-4">
<slot />
</main>Each page's top-level element is <div class="mx-auto max-w-3xl space-y-6">.
At a 390x844 mobile viewport, one page reported window.innerWidth === 714 while visualViewport.width === 390 and visualViewport.scale === 1. The whole page, not just the offending element, laid out at 1.83x the device width, so every element was oversized relative to the screen and the page panned horizontally. <meta name="viewport" content="width=device-width, initial-scale=1"> was present and correct.
The symptom presented as "one card is too wide". It is not: that card merely contains the widest content. main itself measured a correct 390px while its own child measured 697px, which reads as impossible and sends you hunting for a fixed width or a stray min-width that does not exist.
The same component tree on a different route did NOT blow out, which made it look content-dependent rather than structural.
Also present in the same codebase, from the same cause and much easier to miss: another page's mx-auto max-w-3xl container rendered 271px inside 358px of available space. Too narrow, no overflow, no visible defect. Same bug, opposite direction.
Root cause: auto margins in the cross axis disable align-items: stretch.
A flex item of a flex-direction: column container has its width as the cross size. Normally align-items: normal/stretch sizes it to the container. But per CSS Flexbox 8.1 auto margins absorb free space, and an item with a cross-axis auto margin is not stretched. The item falls back to fit-content:
fit-content = min(max-content, max(min-content, available))So the container is sized by its content, not by its parent:
- min-content larger than available (a
white-space: precode block, a wide table) -> container overflows -> Chrome expands the layout viewport to fit ->innerWidthgrows and the entire page scales down relative to the device. - max-content smaller than available -> container is too narrow (the 271-in-358 case).
mx-auto is the whole trigger. It is idiomatic Tailwind for horizontal centering and completely inert in a block container, so it gets copied onto pages that later end up inside a column flex shell, where it silently changes the sizing algorithm.
Crucially, overflow-x: auto on the wide descendant does not save you. The scroll-container "automatic minimum size of 0" rule (CSS Sizing 4.1) applies to flex and grid items. A plain block <pre class="overflow-x-auto"> still propagates its min-content width up the block chain, so it scrolls internally AND widens its ancestors.
Fix
Add w-full to the page container so the width is definite:
- <div class="mx-auto max-w-3xl space-y-6">
+ <div class="mx-auto w-full max-w-3xl space-y-6">One class fixes both directions, the blowout and the too-narrow case. max-w-full also works if you only need the clamp.
Add a shell-level guard so a future page cannot regress it:
<main class="... flex flex-col [&>*]:max-w-full">What does NOT work (measured per-trial on the live page)
| baseline | 714 | 697 |
w-full / max-w-full on the container | 390 | 358 |
min-width: 0 on ANY ancestor (main, container, li, wrapper, the pre itself) | 714 | 697 |
overflow-x: hidden on main | 390 | 697 |
min-w-0 is the reflex fix for flexbox overflow and it is a complete no-op here. It targets the automatic minimum size, but auto margins are what disabled stretch. Do not stop when it fails; the bug is the margin, not the min-size.
overflow-x: hidden on the shell is worse than the bug: innerWidth reads a healthy 390 while the container stays 697px and content is silently clipped with no way to reach it. That is a WCAG 1.4.10 (Reflow) failure, and it hides the defect from exactly the documentElement.scrollWidth check you would use to detect it.
Diagnose it in one pass
innerWidth > visualViewport.width with scale === 1 is the fingerprint of layout-viewport expansion. Then bisect the DOM instead of guessing: hide each child in turn and watch innerWidth.
const base = innerWidth;
let node = document.querySelector('main');
for (let d = 0; d < 12; d++) {
const culprit = [...node.children].find((k) => {
const prev = k.style.display;
k.style.display = 'none';
void document.body.offsetWidth;
const dropped = innerWidth < base;
k.style.display = prev;
void document.body.offsetWidth;
return dropped;
});
if (!culprit) break;
console.log(d, culprit.tagName, culprit.className);
node = culprit;
}This walks straight to the element carrying the min-content, in seconds, with no guessing.
Detection in CI
A desktop viewport will not catch it: with a fixed viewport innerWidth stays put and only documentElement.scrollWidth grows. Emulate a real mobile device (Playwright devices['iPhone 13']) and assert both:
expect(await page.evaluate(() => innerWidth)).toBe(390);
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(390);Asserting only scrollWidth passes on the mobile path, because the layout viewport expanded to match the content and no overflow remains to measure.