Stacking Context and Block Formatting Context
TL;DR
Two invisible layout concepts that explain “why doesn’t z-index work?” and “why won’t this float clear?”. A stacking context is a self-contained z-axis layer — z-index only compares elements within the same context, so a child can never escape its parent’s stacking order. A block formatting context (BFC) is a self-contained layout region that contains floats, stops margin collapse, and prevents content wrapping around floats. Know what creates each and you can debug the two most confusing CSS layout bugs.
Interview Q&A
Q: What is a stacking context and what creates one?
A: A stacking context is a group of elements with a shared z-axis ordering. z-index values only matter relative to siblings in the same context. Common triggers:
- the root
<html>, position: relative/absolutewithz-indexother thanauto,position: fixedorsticky,opacity< 1,transform,filter,perspective,clip-path,mask,will-change(of a property that would create one),isolation: isolate,mix-blend-modeother than normal,contain: layout/paint/strict,- a flex/grid child with
z-indexother thanauto.
Q: The classic z-index bug — explain it.
A: A high z-index element appears behind something with a lower z-index:
.a { position: relative; z-index: 1; opacity: 0.99; } /* opacity<1 → new stacking context */
.a .child { position: relative; z-index: 9999; } /* trapped inside .a's context */
.b { position: relative; z-index: 2; } /* sibling of .a */
.a .child’s 9999 only competes inside .a. Since .a (z-index 1) sits below .b (z-index 2), the child renders behind .b no matter how huge its z-index. The fix: raise .a, or don’t create the trapping context. The opacity: 0.99 (or a transform) is the usual silent culprit.
Q: How do you deliberately create or contain a stacking context?
A: isolation: isolate creates a stacking context with no other side effects — use it to “sandbox” a component’s z-indexes so they don’t leak or get trapped. It’s the clean, intentional trigger (vs. abusing opacity/transform).
Q: What is a Block Formatting Context (BFC)?
A: A region where block-level boxes lay out independently. Inside a BFC: floats are contained, vertical margins don’t collapse across its boundary, and the box doesn’t overlap floats outside it. Triggers:
display: flow-root(the modern, side-effect-free trigger),overflowother thanvisible(hidden/auto),floatother thannone,position: absolute/fixed,display: inline-block, table cells, flex/grid items (they establish their own formatting contexts),contain: layout/content/strict.
Q: What problems does a BFC solve?
A: Three classics:
- Contain floats (modern clearfix) — a parent of only floated children collapses to zero height;
display: flow-rooton the parent makes it wrap them:.parent { display: flow-root; } /* contains floats — no clearfix hack */ - Stop margin collapse — adjacent/parent-child vertical margins merge; putting content in a BFC prevents collapse across the boundary.
- Prevent text wrapping around a float — give the following block its own BFC (
overflow: hidden/flow-root) so it forms a column beside the float instead of wrapping under it.
Q: Why prefer display: flow-root over overflow: hidden for containing floats?
A: overflow: hidden also clips content and can break shadows/tooltips/sticky children; it’s a side effect you’re tolerating to get a BFC. flow-root creates a BFC and nothing else — it’s the purpose-built tool. Use it for clearfix and float containment.
Gotchas / edge cases
transform/opacity/filtersilently create stacking contexts — the #1 cause of “my modal/tooltip renders behind something.” A parent withtransform: translateZ(0)traps its children’s z-index.position: fixedinside a transformed ancestor is positioned relative to that ancestor, not the viewport — another transform side effect.overflow: hiddento clear floats can clip drop-shadows and preventposition: stickychildren from sticking — preferflow-root.- Margin collapse only affects vertical margins of block boxes in the same BFC; flex/grid items never margin-collapse.
- z-index does nothing without
position(or being a flex/grid item) —z-indexon astaticblock is ignored.
What a senior is expected to say
- “z-index only compares within a stacking context; a child can’t escape its parent’s context, so a
transform/opacityon an ancestor is usually why a high z-index ‘doesn’t work.’ I useisolation: isolateto sandbox deliberately.” - “A BFC contains floats, stops margin collapse, and prevents float overlap.
display: flow-rootis the clean trigger — I use it for clearfix instead ofoverflow: hidden, which has clipping side effects.”
Cross-references
- Layout fundamentals (flexbox/grid establish formatting contexts): README.md
containas a perf + context tool: 04_modern_css_and_perf.md- z-index of portals/overlays: ../08_component_libraries/headless_ui_and_radix.md
Further reading
- MDN — Stacking context: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context
- MDN — Block formatting context: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_display/Block_formatting_context
- MDN —
isolation: https://developer.mozilla.org/en-US/docs/Web/CSS/isolation