Skip to content

Block Formatting Context and Stacking Contexts

advanced8 min read

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.

Mental Model

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 */
Use display: flow-root

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
Common Trap

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):

  1. Background and borders of the stacking context root
  2. Negatively positioned stacking contexts (z-index < 0)
  3. In-flow, non-positioned block elements (normal block flow)
  4. Non-positioned floats
  5. In-flow, inline-level elements (text, inline boxes)
  6. Positioned elements with z-index: 0 or auto
  7. 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:

  1. The sidebar had transform: translateX(0) for a slide-in animation — creating a stacking context
  2. The modal was inside a container with z-index: 100
  3. But the sidebar's stacking context had z-index: auto (effectively 0) initially, then z-index: 50 after animation
  4. 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 doWhat 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
Quiz
A div has overflow: visible and contains two floated children. The div collapses to zero height. Which single property addition fixes this?
Quiz
Element A has z-index: 9999 inside a parent with z-index: 1. Element B has z-index: 2 inside a parent with z-index: 10. Which element appears on top?
Key Rules
  1. 1A Block Formatting Context (BFC) is an isolated layout region — floats are contained, margins don't escape.
  2. 2Use display: flow-root to create a BFC without side effects. Avoid overflow: hidden for this purpose.
  3. 3z-index only competes within the same stacking context. Elements in different contexts never interleave.
  4. 4opacity < 1, transform, filter, and mix-blend-mode all silently create new stacking contexts.
  5. 5Use isolation: isolate to explicitly create stacking contexts without visual side effects.
  6. 6Inline elements ignore width, height, and vertical margins. Use inline-block when you need those.