Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Engineering

Engineering

The Flexbox Bug That Won't Let Your Panel Scroll: min-height: 0

THE SHORT VERSION4 points
  • 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's 1fr hides it too. Use minmax(0, 1fr).
  • The tell: you set overflow: auto and 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: hidden on .body also works, since it makes .body a scroll container and drops its minimum to zero. I prefer min-height: 0 because 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: 0 from the start. It costs nothing, and it saves you from rediscovering this bug the next time the layout gets one level deeper.