Stacking Context and Block Formatting Context
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.
What is a stacking context and what creates one?
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.
The classic z-index bug — explain it.
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.
How do you deliberately create or contain a stacking context?
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).
What is a Block Formatting Context (BFC)?
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.
What problems does a BFC solve?
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.
Why prefer display: flow-root over overflow: hidden for containing floats?
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
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.
Related
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
Interview angle 3
- “An element has
z-index: 9999and still sits behind something. Why?” -z-indexonly compares siblings inside the same stacking context, and a child can never escape its parent’s. Some ancestor has atransform, anopacitybelow 1, afilteror awill-change, which created a context and trapped the whole subtree at that ancestor’s level.- Follow-up: “So how do you fix it?” - raise the ancestor rather than the
child, or stop creating the context. And when you want the containment
deliberately,
isolation: isolatecreates one with no other side effects, which is better than reaching for atransformyou do not want.
- Follow-up: “So how do you fix it?” - raise the ancestor rather than the
child, or stop creating the context. And when you want the containment
deliberately,
- “What is a block formatting context, and when do you need one?” - a region that lays out independently: it contains floats, stops margins collapsing through it, and keeps content from wrapping around an outside float. You need one whenever a parent has collapsed to zero height around floated children, or a child’s margin has escaped its parent.
- “How do you trigger one?” -
display: flow-root, which does nothing else.overflow: hiddenalso works and is the old clearfix, but it clips content and breaks sticky positioning and outside shadows, so it fixes one problem by quietly introducing another.