Frontend / CSS / 01_stacking_context_and_bfc.md

Stacking Context and Block Formatting Context

Updated 3 interview angles 4 min read source
On this page10
  1. What is a stacking context and what creates one?
  2. The classic z-index bug — explain it.
  3. How do you deliberately create or contain a stacking context?
  4. What is a Block Formatting Context (BFC)?
  5. What problems does a BFC solve?
  6. Why prefer display: flow-root over overflow: hidden for containing floats?
  7. Gotchas
  8. Related
  9. Further reading
  10. Interview angle

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

The classic z-index bug — explain it.

A high z-index element appears behind something with a lower z-index:

css
.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),
  • 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.

What problems does a BFC solve?

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:
    css
    .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.

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

Further reading

Interview angle 3

  • “An element has z-index: 9999 and still sits behind something. Why?” - z-index only compares siblings inside the same stacking context, and a child can never escape its parent’s. Some ancestor has a transform, an opacity below 1, a filter or a will-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: isolate creates one with no other side effects, which is better than reaching for a transform you do not want.
  • “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: hidden also 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.