A block formatting context fixes collapsed parents, escaping margins and content running under floats. We measured the common triggers in three browser engines, and what each one breaks.
Try each trigger
The sentences under each box are measured in the browser you are reading this in.
Float inside
Child with a margin
Float beside
What each trigger does
We put each trigger on the same test box and measured four things. Did the box grow around a 100 pixel float? Did a child’s 30 pixel margins stay inside it? Did it move aside for a float next to it? Did its own margins still collapse with the boxes above and below? Chrome, Firefox and WebKit gave the same answer in every cell.
| CSS on the box | Contains a float | Keeps child margins | Beside a float | Own margins collapse |
|---|---|---|---|---|
| Plain block | No | No | No | Yes |
| display: flow-root | Yes | Yes | Yes | Yes |
| overflow: hidden | Yes | Yes | Yes | Yes |
| overflow: auto | Yes | Yes | Yes | Yes |
| overflow: clip | No | No | No | Yes |
| float: left | Yes | Yes | Yes | No |
| position: absolute | Yes | Yes | No | No |
| display: inline-block | Yes | Yes | Yes | No |
| Flex item | Yes | Yes | N/A | No |
| Grid item | Yes | Yes | N/A | No |
| contain: layout | Yes | Yes | Yes | Yes |
| contain: paint | Yes | Yes | Yes | Yes |
| container-type: inline-size | Yes | Yes | Yes | Yes |
| align-content: center | Yes | Yes | Yes | Yes |
| Clearfix (::after with clear: both) | Yes | Bottom only | No | Yes |
| padding: 1px 0 | No | Yes | No | Yes |
overflow: clip clips like hidden and starts no context at all, which the spec says outright. The box measured 0 pixels tall around its float. position: relative, transform and opacity behaved exactly like a plain block in all four tests. A pixel of padding keeps child margins in, because it separates them from the box’s edge, and leaves floats alone.
Two triggers can arrive by accident. Adding container-type: inline-size for a container query starts a context, as the draft spec requires. So does any align-content value other than normal, by the alignment spec. Text that used to wrap around a float stops wrapping once either one lands on its box.
display: flex and display: grid on the box gave the same results as flow-root in the float and child margin tests. They start a flex or grid formatting context instead, so the rules inside are different.
Which margins still collapse
A context only stops collapse across its own edge, between the box and its children. A flow-root box with a 30 pixel top margin under a 20 pixel bottom margin still sat 30 pixels from its neighbour, and a 10 against 40 pair below it gave 40. Inside the box, two children with 20 and 30 pixel margins were 30 apart in every row of the table above.
Floats, inline-blocks and flex or grid items don’t collapse with their neighbours. The same 20 and 30 added up to 50 for each. To stop two stacked sections collapsing, put them in a flex or grid column. flow-root on one of them won’t do it.
The three bugs it fixes
A parent collapses around its floats
A plain box containing only a float measured 0 pixels tall, so its background and border vanish and the next element rides up. With flow-root it measured 100.
A child’s margin escapes the parent
A heading’s top margin can push its whole card down and leave the card’s background starting below the gap. In the test, the plain box kept 0 of the child’s 30 pixels and flow-root kept all of them.
Content runs under a float
A plain box beside a 100 pixel float in a 400 pixel container started at the left edge and was 400 wide, with the float painted over it. With flow-root it started at 100 and was 300 wide. A box with a fixed width of 350 no longer fit, so it dropped to 100 pixels down, below the float. Inside a context, clear: both also ignores floats outside the box: a cleared child sat at the top instead of 100 pixels down.
/* The default choice */
.media-body { display: flow-root; }
/* When the box also has to clip, such as rounded corners */
.card { display: flow-root; overflow: clip; border-radius: 12px; }
Photo by Caleb OquendoWhat the triggers cost
We measured each side effect in the same three browsers. The last column says what a position: fixed child with top: 0 was placed against.
| CSS on the box | Sticky child | Outline and shadow | Fixed child |
|---|---|---|---|
| display: flow-root | Sticks | Painted | Viewport |
| overflow: hidden | Scrolls away | Clipped | Viewport |
| overflow: auto | Scrolls away | Clipped | Viewport |
| overflow: clip | Sticks | Clipped | Viewport |
| display: flow-root with overflow: clip | Sticks | Clipped | Viewport |
| float: left | Sticks | Painted | Viewport |
| position: absolute | Sticks | Painted | Viewport |
| display: inline-block | Sticks | Painted | Viewport |
| contain: layout | Sticks | Painted | Box |
| contain: paint | Sticks | Clipped | Box |
overflow: hidden and auto turn the box into a scroll container, so a sticky child inside it stops following the window. After scrolling the page 500 pixels, the child sat at 500 pixels above the viewport.
hidden also doesn’t stop scrolling. Calling focus() on an input below the box’s edge scrolled it by about 220 pixels in all three browsers, and with overflow: clip it stayed at 0. hidden clipped an absolutely positioned child only when the box had position: relative; contain: paint clipped it either way.
With content spilling sideways, overflow: auto grew a 20 pixel box to 35 in Chrome by adding a scrollbar. Firefox and WebKit showed no scrollbar in this headless setup. A scrollable auto box became a tab stop in Chrome and Firefox and did not in WebKit.
contain: layout makes the box the containing block for fixed and absolute children and starts a stacking context, as the spec describes. Floats, absolute boxes and inline-blocks shrank to their child’s 50 pixels inside a 400 pixel container. An absolute box also left the flow, so its container measured 0 tall, and an inline-block left a gap of 4 pixels below it (3.5 in Firefox) for the text baseline.
overflow: hidden on body starts no context, because the value moves to the viewport. body measured 0 pixels around a float until html got an overflow value of its own, or body got display: flow-root. Both gave 100.
How this was tested
A script gave each trigger a fresh page in a 1000 by 800 pixel window on Windows 11 on 8 November 2026. Layout numbers are from getBoundingClientRect, clipping from elementFromPoint, and outlines and shadows from pixels in a screenshot.
- Chrome 156.0.8078.80, headless, through Playwright, with scrollbars left on.
- Firefox 159.0.1, headless, through Puppeteer.
- WebKit 26.0 from Playwright. This is not Safari, and nothing here ran on an iPhone.
Flex and grid items can’t have a float beside them, so those cells are not applicable. The full output is available as raw JSON. The test script and this article were prepared with help from AI.
How this used to work
Internet Explorer had its own version of the idea, a property called hasLayout. Microsoft’s reference lists inline-block, float, position: absolute, any width or height, and zoom as the values that turned it on, which is why zoom: 1 turns up in old stylesheets. Only the first three still have this effect. width: 300px and zoom: 1 matched a plain block in our run.
Common questions
Should I replace clearfix?
Yes, where you touch that code. Clearfix contained the float in our test, but the child’s top margin still escaped. Its clear: both also reached a float outside the box, which stretched a 20 pixel box to 100.
Why does overflow: hidden fix float problems?
Any overflow value except visible and clip starts a context, and the context contains the floats. The clipping comes with it whether you want it or not.
Is display: flow-root safe to use everywhere?
In our run it contained floats and kept margins in all three browsers, and it was the only trigger that matched a plain block in every side effect measured.
