Block Formatting Context and Stacking Contexts
The Invisible Rules That Govern Layout
Most CSS layout bugs — collapsed margins, floats escaping containers, z-index battles — trace back to two concepts that most developers never learn properly: Block Formatting Contexts (BFC) and Stacking Contexts. These are the layout engine's internal rules for how elements flow horizontally and how they layer vertically.
Think of a BFC as a fenced yard. Everything inside the fence follows its own layout rules — floats are contained, margins don't escape, and the fence itself participates in its parent's layout as a single unit. Without the fence, children can "leak" outside (floats) or interact with neighbors (margin collapse). A Stacking Context is like a transparent sheet of acetate in a stack of animation cels. You can rearrange layers within one sheet (z-index), but one sheet can never interleave with layers on another sheet. The sheets themselves are ordered.
Block Formatting Context (BFC)
A Block Formatting Context is an isolated layout region where block-level elements are laid out. Inside a BFC:
- Block elements stack vertically, one after another
- Each element's left edge touches the containing block's left edge
- Vertical margins between siblings collapse (unless a BFC boundary separates them)
- The BFC contains all its floated children — floats don't escape
- The BFC does not overlap adjacent floats
What Creates a New BFC?
Not every element creates a BFC. Here are the most common triggers:
/* Root element always creates a BFC */
html { }
/* overflow value other than visible */
.container { overflow: hidden; } /* Creates BFC */
.container { overflow: auto; } /* Creates BFC */
.container { overflow: visible; } /* Does NOT create BFC */
/* display: flow-root — purpose-built for creating BFCs */
.container { display: flow-root; } /* Creates BFC — the clean solution */
/* Flex and grid items */
.flex-container { display: flex; } /* Each flex item is a BFC */
/* Floated elements */
.sidebar { float: left; } /* Creates BFC */
/* Absolutely/fixed positioned elements */
.modal { position: absolute; } /* Creates BFC */
/* Inline-blocks */
.badge { display: inline-block; } /* Creates BFC */
display: flow-root was introduced specifically to create a BFC without side effects. Before it existed, developers used overflow: hidden (which clips content) or overflow: auto (which can show scrollbars). flow-root is the correct, semantic way to establish a BFC in modern CSS.
BFC Solves: Float Containment
The classic "collapsed container" problem. A parent contains only floated children, so it collapses to zero height:
<div class="container">
<div class="sidebar" style="float: left; width: 200px; height: 300px;">Sidebar</div>
<div class="content" style="float: left; width: 400px; height: 200px;">Content</div>
</div>
<!-- .container has height: 0 because floats are "out of flow" -->
Fix: Make the container a BFC.
.container {
display: flow-root; /* Now contains its floated children, height: 300px */
}
BFC Solves: Margin Collapse Prevention
Adjacent vertical margins collapse into one (the larger wins). A BFC boundary prevents this:
.parent {
margin-bottom: 20px;
}
.child {
margin-top: 30px;
}
/* Without BFC: margins collapse to 30px total gap */
/* With BFC on parent (display: flow-root): 20px + 30px = 50px total */
Inline Formatting Context (IFC)
When a container holds only inline-level content (text, <span>, <a>, <em>, inline images), it creates an Inline Formatting Context. In an IFC:
- Elements are laid out horizontally, left to right
- When a line is full, content wraps to the next line
- Vertical alignment is controlled by
vertical-align - Line boxes have a computed height based on the tallest inline element on that line
- Vertical margins and padding on inline elements do not affect line height
You cannot set width or height on inline elements. margin-top and margin-bottom are ignored. padding-top and padding-bottom are applied visually but do not push other lines away — they overlap. If you need vertical spacing on an inline element, use display: inline-block (which creates a BFC) or line-height.
Stacking Contexts
While BFC controls horizontal flow, Stacking Contexts control vertical layering — how elements overlap when they share the same space.
The Paint Order Within a Stacking Context
Elements within a single stacking context are painted in this exact order (back to front):
- Background and borders of the stacking context root
- Negatively positioned stacking contexts (z-index < 0)
- In-flow, non-positioned block elements (normal block flow)
- Non-positioned floats
- In-flow, inline-level elements (text, inline boxes)
- Positioned elements with z-index: 0 or auto
- Positively positioned stacking contexts (z-index > 0)
What Creates a New Stacking Context?
/* position + z-index (not auto) */
.modal { position: relative; z-index: 10; } /* Creates stacking context */
.modal { position: relative; } /* Does NOT (z-index is auto) */
/* Certain properties always create one */
.card { opacity: 0.99; } /* Creates stacking context! */
.animated { transform: scale(1); } /* Creates stacking context */
.blurred { filter: blur(0px); } /* Creates stacking context */
.clipped { clip-path: none; } /* Does NOT create one unless clipping */
.mixed { mix-blend-mode: multiply; } /* Creates stacking context */
.isolated { isolation: isolate; } /* Creates stacking context (purpose-built) */
.sticky { position: sticky; } /* Creates stacking context */
/* Flex/grid children with z-index */
.flex-child { z-index: 1; } /* Creates stacking context even without position */
/* will-change with certain properties */
.animated { will-change: transform; } /* Creates stacking context */
Why opacity creates a stacking context
This catches many developers off guard. Setting opacity to any value less than 1 (even 0.999) forces the browser to create a new stacking context. Why? Because opacity applies to the element AND all its children as a group — the browser must composite them together onto a single layer before applying the transparency. This group compositing requires a stacking context. The same logic applies to filter, transform, and mix-blend-mode — they all need to group-composite children before applying the effect.
Why z-index: 9999 Doesn't Work
The most common stacking context bug: z-index only works within the same stacking context. Elements in different stacking contexts can never interleave.
/* Parent A creates stacking context at z-index: 1 */
.sidebar { position: relative; z-index: 1; }
/* Child: z-index: 9999, but trapped inside sidebar's stacking context */
.sidebar .tooltip { position: absolute; z-index: 9999; }
/* Parent B creates stacking context at z-index: 2 */
.main-content { position: relative; z-index: 2; }
/* The tooltip (z-index: 9999) will ALWAYS render behind .main-content
because its parent stacking context (z-index: 1) is below
main-content's stacking context (z-index: 2).
z-index: 9999 is meaningless outside its parent context. */
The fix: Move the tooltip to the same stacking context level as main-content, or use isolation: isolate strategically to control context creation.
Production Scenario: The Modal Behind the Sidebar
A web app had a modal that appeared behind the sidebar on certain pages but not others. The root cause:
- The sidebar had
transform: translateX(0)for a slide-in animation — creating a stacking context - The modal was inside a container with
z-index: 100 - But the sidebar's stacking context had
z-index: auto(effectively 0) initially, thenz-index: 50after animation - On pages where the sidebar was animated, it created its own stacking context, and the modal's parent context was below the sidebar's
The fix: Use isolation: isolate on the app shell to create a controlled stacking context hierarchy, and portal the modal to document.body so it sits at the root stacking context level.
| What developers do | What they should do |
|---|---|
| Use overflow: hidden to create a BFC overflow: hidden clips overflowing content, which can hide tooltips, shadows, and positioned children | Use display: flow-root for BFC creation |
| Keep adding higher z-index values to fix stacking z-index only competes within the same stacking context — 9999 inside a lower context still loses | Understand which stacking contexts exist and work within them |
| Forget that opacity, transform, and filter create stacking contexts These properties silently create stacking contexts, trapping child z-index values | Check if your element already creates a stacking context before debugging z-index |
| Set vertical margin/padding on inline elements and expect spacing Inline elements ignore vertical margins and their vertical padding doesn't affect layout | Use display: inline-block or line-height for vertical spacing on inline elements |
- 1A Block Formatting Context (BFC) is an isolated layout region — floats are contained, margins don't escape.
- 2Use display: flow-root to create a BFC without side effects. Avoid overflow: hidden for this purpose.
- 3z-index only competes within the same stacking context. Elements in different contexts never interleave.
- 4opacity < 1, transform, filter, and mix-blend-mode all silently create new stacking contexts.
- 5Use isolation: isolate to explicitly create stacking contexts without visual side effects.
- 6Inline elements ignore width, height, and vertical margins. Use inline-block when you need those.