Engineering
The Flexbox Bug That Won't Let Your Panel Scroll: min-height: 0
- Flex items default to
min-height: auto, which means never smaller than my content, so a plain wrapper can outgrow a 100vh layout. - The scroller itself is fine. It's the wrapper above it that needs
min-height: 0. - Rows have the mirror-image bug with
min-width, and grid's1frhides it too. Useminmax(0, 1fr). - The tell: you set
overflow: autoand the thing grows instead of scrolling.
I ran into this one while building Kaybl, my cable-TV-simulator web app. A channel guide is exactly the kind of screen that triggers it: an app-style layout that fills the window, with a big area in the middle that's supposed to scroll on its own while everything around it stays put. Flexbox, a height: 100vh, an overflow: auto on the part that scrolls. Should be done.
Instead, this bug makes the page scroll. The header slides off the top, the layout grows as tall as its content, and the overflow: auto sits there doing nothing at all.
If you've built an app-style layout with nested flexboxes, you've probably met this bug too. It has a one-line fix, and a reason that makes sense once you see it.
The repro
Here's the smallest version I can make that shows it:
<div class="app">
<header class="top">Header</header>
<main class="body">
<aside class="side">Sidebar</aside>
<section class="guide">
<h2 class="guide-title">Guide</h2>
<div class="rows">
<!-- a hundred rows of listings -->
</div>
</section>
</main>
</div>
.app { display: flex; flex-direction: column; height: 100vh; }
.top { flex: none; height: 60px; }
.body { flex: 1; display: flex; }
.side { flex: none; width: 200px; }
.guide { flex: 1; display: flex; flex-direction: column; }
.guide-title { flex: none; }
.rows { flex: 1; overflow-y: auto; }
Read it top to bottom and it looks right. .app is exactly the viewport's height. .body takes whatever's left after the header. .rows fills what's left of the guide and scrolls.
In a 600-pixel-tall window, though, .body comes out around 5,000 pixels tall, .rows is exactly as tall as its content, and there's nothing for it to scroll.
Why it happens
Flex items have a default you've probably never set on purpose: min-height: auto (and min-width: auto). For most elements auto there means zero. For a flex item, the spec defines it as an automatic minimum size, which for an ordinary item works out to roughly "as big as my content."
So flex: 1 on .body says "take the leftover space," but the automatic minimum says "and never get smaller than the content inside me." When the content is a hundred rows, the minimum wins. .body grows, .app's children overflow it, and the page scrolls instead of the panel.
The interesting part is that .rows itself is fine. The automatic minimum doesn't apply to scroll containers, so anything with overflow set to something other than visible already gets a minimum of zero. The problem is the plain wrapper above it, sitting between the fixed-height container and the scroller, still insisting on being content-sized.
The fix
Tell that wrapper it's allowed to shrink:
.body { flex: 1; display: flex; min-height: 0; }
That's it. .body now fits in the space the column gives it, .guide stretches to match, and .rows finally has a fixed height to scroll within.
The general rule: every flex item in a column between the fixed-height ancestor and the scrolling element needs min-height: 0, or some overflow value that isn't visible.
In this repro only .body needs it, because .guide sits in a row container and gets its height by stretching across, not along the main axis. Nest another column wrapper in there and that one would need it too. That's why this bug seems to come back every time the layout gets one level deeper.
Note
overflow: hiddenon.bodyalso works, since it makes.bodya scroll container and drops its minimum to zero. I prefermin-height: 0because it says what I mean and doesn't risk clipping focus rings or dropdowns later.
The sideways version
The same default exists in the other direction as min-width: auto, and it bites in row layouts. A long unbreakable thing (a URL, a table, a line of code) makes a flex item refuse to get narrower than it, so it shoves its neighbors over.
This blog hit exactly that. A long line in a code block pushed the article column wider than it should have been and shoved the layout into the sidebar, even though the code block had overflow-x: auto. Same cause: the code block could scroll, but the column wrapping it wasn't allowed to shrink below the width of that line.
For a flex row, the fix is the mirror image:
.content { flex: 1; min-width: 0; }
Grid has the same trap in a slightly different costume. A 1fr track is really minmax(auto, 1fr), and that auto minimum is content-sized. Either put min-width: 0 on the grid item, or define the track so it can shrink:
.layout { display: grid; grid-template-columns: minmax(0, 1fr) 250px; }
| Layout | Symptom | Fix |
|---|---|---|
| Flex column | Wrapper grows taller than its container | min-height: 0 on each wrapper between the fixed height and the scroller |
| Flex row | Long content shoves neighbors over | min-width: 0 on the item |
| Grid | 1fr track won't shrink below its content |
min-width: 0 on the item, or minmax(0, 1fr) for the track |
How to spot it next time
The tell is always the same: you set overflow: auto on something, and instead of scrolling, it (or its parent) just grows.
When that happens, open the dev tools, walk up from the scroller, and look at each flex or grid item between it and the thing with the fixed size. The first one that's taller or wider than its container is the one that wants min-height: 0 or min-width: 0.
Tip
If a flex wrapper exists only to hold a scroll area, it's worth giving it
min-height: 0from the start. It costs nothing, and it saves you from rediscovering this bug the next time the layout gets one level deeper.
- Kaybl: One Cable Guide, Three ScreensWeb Design