DNAðŸŽĻ CSSZ-Index & Stacking Contexts
ðŸĢHatchlingCSSStacking ContextZ-IndexLayout

Z-Index & Stacking Contexts

Z-index only works within stacking contexts. Misunderstanding stacking contexts is behind most z-index bugs — adding z-index: 9999 is a symptom, not a solution.

Z-Index & Stacking Contexts

Every z-index war in a codebase traces back to one misunderstanding: z-index doesn't create a global layer system. It only orders elements within the same stacking context. A z-index: 9999 inside one context can be painted behind a z-index: 1 in a parent context.

The Default Stacking Order

Without z-index, elements paint in this order (back to front):

1. Root element background and borders
2. Descendant non-positioned elements (in DOM order)
3. Descendant positioned elements (in DOM order)
<div class="red" style="position: relative;">I appear on top</div>
<div class="blue">I appear behind, even though I'm after in DOM</div>

Positioned elements always paint above non-positioned ones, regardless of DOM order.

What Creates a Stacking Context

A stacking context is created by any of these conditions:

TriggerExample
Root element<html>
position + z-index (not auto)position: relative; z-index: 1
position: fixed or position: stickyAlways creates context
opacity less than 1opacity: 0.99
transform (any value)transform: translateZ(0)
filter (any value)filter: blur(0)
will-change (transform, opacity, etc.)will-change: transform
isolation: isolateExplicit stacking context
contain: layout or contain: paintCSS containment
Flex/grid child with z-index (not auto)display: flex parent, child has z-index
mix-blend-mode (not normal)Compositing operations
clip-pathAny clip-path value
maskAny mask value

The Common Trap

.parent {
  position: relative;
  z-index: 1;    /* Creates stacking context */
}
.child {
  position: absolute;
  z-index: 9999; /* Only competes within .parent's context */
}
.sidebar {
  position: relative;
  z-index: 2;    /* Beats .parent (and all its children) */
}

The child's z-index: 9999 is irrelevant — it can never paint above .sidebar because .parent's z-index: 1 loses to .sidebar's z-index: 2. The child is trapped.

isolation: isolate

The cleanest way to create a stacking context without side effects:

.modal-container {
  isolation: isolate;
}

Unlike transform: translateZ(0) or opacity: 0.99, isolation: isolate has no visual side effects. It exists solely to create a stacking context.

Component Isolation Pattern

.component {
  isolation: isolate;
}

Every self-contained component should create its own stacking context. This prevents internal z-index values from leaking out and conflicting with siblings.

.card {
  isolation: isolate;
}
 
.card__overlay {
  position: absolute;
  z-index: 10;
}
 
.card__badge {
  position: absolute;
  z-index: 20;
}

These z-index values are local to .card — they cannot interfere with anything outside.

Z-Index Scale Convention

Instead of arbitrary numbers, define a scale:

:root {
  --z-below: -1;
  --z-default: 0;
  --z-raised: 1;
  --z-dropdown: 100;
  --z-sticky: 200;
  --z-overlay: 300;
  --z-modal: 400;
  --z-popover: 500;
  --z-toast: 600;
  --z-tooltip: 700;
}
 
.dropdown { z-index: var(--z-dropdown); }
.modal    { z-index: var(--z-modal); }
.toast    { z-index: var(--z-toast); }

This eliminates the "add one more" pattern that leads to z-index: 99999.

Debugging Stacking Contexts

In Chrome DevTools:

  1. Elements panel → select an element
  2. Check the "Computed" tab → search for z-index
  3. The Layers panel (More Tools → Layers) visualizes 3D stacking

The mental model for debugging:

1. Is the element positioned? (z-index only works on positioned, flex/grid children)
2. What stacking context is it in? (Walk up the tree)
3. What are the z-index values of sibling stacking contexts?
4. Is an ancestor creating an unexpected stacking context? (opacity, transform, filter)

Common z-index Bugs

Modal Behind Overlay

.page-content {
  position: relative;
  z-index: 1;        /* Creates context — traps everything inside */
}
.modal {
  position: fixed;
  z-index: 99999;    /* Stuck inside .page-content's context */
}

Fix: Remove unnecessary z-index from .page-content, or move the modal outside the stacking context in the DOM (portal pattern in React).

Transform Creating Unwanted Context

.parent {
  transform: translateX(0);  /* Creates stacking context! */
}
.child {
  position: fixed;
  /* Now "fixed" is relative to .parent, not viewport */
  /* z-index trapped inside .parent */
}

Fix: Avoid applying transform to ancestors of position: fixed elements.

Interview Signal

Senior candidates demonstrate:

  1. Stacking context model — Can enumerate what creates contexts and explain why z-index values are local, not global
  2. isolation: isolate — Knows the clean way to create contexts for component encapsulation
  3. Debugging approach — Walks up the tree to find the context, doesn't add larger z-index values
  4. Scale convention — Uses token-based z-index systems, not arbitrary escalating numbers
  5. Portal awareness — Understands that React portals solve the DOM-nesting stacking context trap