In an app shell built from nested flex-direction: column containers, a panel with overflow-y: auto never scrolls — the whole page grows instead. What causes this and how do you fix it?
answer
- nothing bounds the scroller's height
- the constraint stops at a content floor
- min-height: auto on every column item
- flex: 1 sets basis, not the minimum
- release it at every level of the chain
basics
~20 sEach column flex item defaults to min-height: auto, so it cannot be shorter than its content and grows instead of bounding the scroller. Add min-height: 0 (or a non-visible overflow) on every flex item between the height-constrained ancestor and the scrolling panel.
solid answer
~50 sThe scroller only scrolls if it has a bounded height, and in a column flex chain nothing bounds it. Every flex item keeps `min-height: auto`, which for a flex item means the content-based automatic minimum size, so each intermediate item refuses to be shorter than its content and pushes its own parent taller — the constraint never reaches the panel. Two things must be true: the top of the chain needs a definite height (`100dvh` on the shell, for example), and **every** flex item between that ancestor and the scroller needs `min-height: 0` or an overflow other than `visible`. `flex: 1` alone does not do it — that is `1 1 0%`, which changes the basis but leaves the automatic minimum in place. Missing one level in a deep chain is the usual reason the fix "does not work", so I walk the chain in devtools and check each computed `min-height`.
code
css · 18 lines.shell {
display: flex;
flex-direction: column;
height: 100dvh;
}
.body {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0; /* without this, .body is held open by its content */
}
.list {
flex: 1;
min-height: 0;
overflow-y: auto;
}go deeper
Recognise the shape of the bug: an inner panel that should scroll makes the whole page scroll instead. Know that min-height: 0 on the flex items above it is the usual cure.
Explain the mechanism: min-height: auto on a flex item means a content-based minimum, so the item cannot be shorter than its content and the height constraint never reaches the scroller. State why flex: 1 does not help.
Walk the diagnosis: confirm a definite height at the top of the chain, then read computed min-height on every ancestor to find the level still at auto. Discuss the tradeoff between min-height: 0 and overflow: hidden, including clipped popovers.
Own it at the system level: make the app-shell scroll contract a reviewed layout primitive rather than a fix rediscovered per feature, and decide how nested library wrappers are prevented from silently reintroducing an auto minimum.
## The setup that breaks A typical app shell: a full-height column with a header, a body that should take the remaining space, and inside the body a list that should scroll while the header stays put. ```css .shell { display: flex; flex-direction: column; height: 100dvh; } .body { display: flex; flex-direction: column; flex: 1; } .list { flex: 1; overflow-y: auto; } ``` It looks right and it does not work: `.list` grows to its full content height, `.body` grows with it, `.shell` overflows `100dvh`, and the *page* scrolls instead of the list. ## Why the height constraint never arrives A scroll container scrolls when its content is taller than its own box. So `.list` must have a bounded height. In a column flex layout an item's height is negotiated by flex layout — but that negotiation is clamped by the item's minimum size. On a flex item, `min-height` keeps its initial value `auto`, and for flex items `auto` means the **automatic minimum size**: a content-based floor, roughly the item's min-content height. In a column container that is the height of all its content stacked up. So flex layout says "you may be 400px tall", the minimum says "I am at least 2000px tall", and the minimum wins. The item overflows its parent, and because that parent is itself a flex item with the same default, the swelling propagates all the way up until something has a real constraint — usually the viewport, which then scrolls. ## The fix, stated precisely 1. **Anchor the chain.** The outermost container needs a definite height: `height: 100dvh`, a fixed pixel height, or itself being a properly constrained flex item. Without that, there is no total to distribute and everything is content-sized by definition. 2. **Release the minimum at every level.** Each flex item between the anchor and the scroller needs `min-height: 0` (column direction) so it can be smaller than its content: ```css .shell { display: flex; flex-direction: column; height: 100dvh; } .body { display: flex; flex-direction: column; flex: 1; min-height: 0; } .list { flex: 1; min-height: 0; overflow-y: auto; } ``` `overflow: hidden` on the intermediate items works too, because the spec zeroes the automatic minimum size when the item's computed overflow in that axis is not `visible` — which is also why the scroller itself often needs no extra `min-height: 0`, its own `overflow-y: auto` already did the job. Writing it anyway is harmless and documents the intent. ## Why flex: 1 is not enough This is the single most common misconception. `flex: 1` expands to `flex-grow: 1; flex-shrink: 1; flex-basis: 0%`. It makes the item *want* to be zero-and-then-grow, which sounds like exactly what you need. But `flex-basis` is the starting size for free-space distribution; the automatic minimum size is applied afterwards as a hard clamp. A basis of `0%` with a min-content floor of 2000px still yields 2000px. Basis and minimum are separate machinery, and only one of them is the problem. ## Diagnosing it in a real tree The fix fails in deep trees when one level is missed — a wrapper `<div>` someone added for a context provider, a layout component from a shared library, a fragment that became a real element. The reliable procedure: 1. Select the scroller in devtools and walk up the ancestor chain. 2. For each ancestor that is a flex item, read its **computed** `min-height`. Any `auto` in the chain is a suspect. 3. Confirm by checking heights: the first ancestor whose used height exceeds its parent's is where the constraint broke. A useful temporary probe is to set `min-height: 0` on every element in the subtree from devtools; if the layout snaps into place, you know the class of bug and can then narrow it to the one level that mattered. ## The row-direction twin The identical bug exists on the horizontal axis with `min-width: auto`: a horizontally scrolling strip inside nested row containers refuses to scroll and blows the layout wide instead. Same reasoning, same fix with `min-width: 0`. Being able to state the rule axis-independently — *a flex item's automatic minimum size on the main axis blocks any constraint from reaching its descendants* — is what separates knowing the incantation from understanding it. ## Related traps in the same layout - A `height: 100%` on an intermediate div resolves against a parent with no definite height and therefore does nothing; in a flex chain, `flex: 1` plus `min-height: 0` is the right tool. - Percentage padding on an item in the chain resolves against the **inline** size, not the block size, which produces confusing extra space when you are already fighting heights. - If the shell uses `height: 100vh` rather than `100dvh`, mobile browser toolbars will make the shell taller than the visible viewport, producing a page-level scroll that looks like the same bug but is not.
- Why doesn't flex: 1 on its own solve this?`flex: 1` is `flex-grow: 1; flex-shrink: 1; flex-basis: 0%`. It changes the size the item starts from during free-space distribution, but the automatic minimum size is applied afterwards as a clamp. A zero basis with a content-based floor still resolves to the content's height, so the item never shrinks.
- Is there an equivalent problem for a horizontally scrolling strip?Yes — the same mechanism on the other axis. Inside nested `flex-direction: row` containers, `min-width: auto` keeps each item at least as wide as its content, so an `overflow-x: auto` strip never gets a bounded width and the layout widens instead. The fix is `min-width: 0` on each row-direction item in the chain.
- Someone adds min-height: 0 and it still does not scroll. What do you check next?First, that the top of the chain actually has a definite height — without one there is nothing to distribute. Second, that no level was missed: walk the ancestors in devtools and read each computed `min-height`, since one library wrapper still at `auto` is enough to hold the whole chain open.
- Does overflow: hidden on an intermediate item work as well as min-height: 0?Yes, for this purpose. A computed overflow other than `visible` in that axis makes the automatic minimum size zero, so the item can shrink. The difference is the side effect: it also clips descendants that intentionally paint outside the box, such as focus rings, dropdowns, or shadows, so `min-height: 0` is the safer default.
saying these in an interview costs you the question
- Assuming flex: 1 removes the item's minimum height
- Adding min-height: 0 to only the scroller and stopping there
- Using height: 100% on an ancestor with no definite height
- Blaming overflow-y: auto rather than the unbounded height
- Treating min-width: 0 as the fix in a column-direction chain