Dotted pixel art of a red tulip field running to a flat horizon under a mustard yellow and violet sunset sky.

Block formatting contexts in CSS

Author
gunlaug team
Updated
8 Nov 2026
Reading time
7 min

Photo by Cor Meenderinck

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.

CSS on the outlined box

Float inside

Measuring

Child with a margin

Measuring

Float beside

Measuring

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.

Results for each trigger in Chrome, Firefox and WebKit
CSS on the boxContains a floatKeeps child marginsBeside a floatOwn margins collapse
Plain blockNoNoNoYes
display: flow-rootYesYesYesYes
overflow: hiddenYesYesYesYes
overflow: autoYesYesYesYes
overflow: clipNoNoNoYes
float: leftYesYesYesNo
position: absoluteYesYesNoNo
display: inline-blockYesYesYesNo
Flex itemYesYesN/ANo
Grid itemYesYesN/ANo
contain: layoutYesYesYesYes
contain: paintYesYesYesYes
container-type: inline-sizeYesYesYesYes
align-content: centerYesYesYesYes
Clearfix (::after with clear: both)YesBottom onlyNoYes
padding: 1px 0NoYesNoYes

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; }
Dotted pixel art of pale pink and grey clouds above a band of bright teal and turquoise.Photo by Caleb Oquendo

What 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.

Side effects of each trigger
CSS on the boxSticky childOutline and shadowFixed child
display: flow-rootSticksPaintedViewport
overflow: hiddenScrolls awayClippedViewport
overflow: autoScrolls awayClippedViewport
overflow: clipSticksClippedViewport
display: flow-root with overflow: clipSticksClippedViewport
float: leftSticksPaintedViewport
position: absoluteSticksPaintedViewport
display: inline-blockSticksPaintedViewport
contain: layoutSticksPaintedBox
contain: paintSticksClippedBox

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.

Sources