frontend / css / 01_stacking_context_and_bfc.md

Stacking Context and Block Formatting Context

4 min read source

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/absolute with z-index other than auto,
  • position: fixed or sticky,
  • opacity < 1,
  • transform, filter, perspective, clip-path, mask,
  • will-change (of a property that would create one), isolation: isolate, mix-blend-mode other than normal,
  • contain: layout/paint/strict,
  • a flex/grid child with z-index other than auto.

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),
  • overflow other than visible (hidden/auto),
  • float other than none,
  • 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:

  1. Contain floats (modern clearfix) — a parent of only floated children collapses to zero height; display: flow-root on the parent makes it wrap them:
    .parent { display: flow-root; }   /* contains floats — no clearfix hack */
  2. Stop margin collapse — adjacent/parent-child vertical margins merge; putting content in a BFC prevents collapse across the boundary.
  3. 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/filter silently create stacking contexts — the #1 cause of “my modal/tooltip renders behind something.” A parent with transform: translateZ(0) traps its children’s z-index.
  • position: fixed inside a transformed ancestor is positioned relative to that ancestor, not the viewport — another transform side effect.
  • overflow: hidden to clear floats can clip drop-shadows and prevent position: sticky children from sticking — prefer flow-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-index on a static block 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/opacity on an ancestor is usually why a high z-index ‘doesn’t work.’ I use isolation: isolate to sandbox deliberately.”
  • “A BFC contains floats, stops margin collapse, and prevents float overlap. display: flow-root is the clean trigger — I use it for clearfix instead of overflow: hidden, which has clipping side effects.”

Cross-references

Further reading