Z-Index & Stacking Context

Difficulty: Intermediate

z-index controls the stacking order of elements along the z-axis (the axis that points toward the viewer). It only works on positioned elements (anything other than position: static) and elements within a flex or grid container. A higher z-index value means the element appears in front of elements with lower values. Understanding z-index requires understanding stacking contexts, which is one of the most misunderstood concepts in CSS.

The default stacking order without z-index follows specific rules. Non-positioned elements paint in document order (later elements paint on top). Positioned elements paint on top of non-positioned elements. Among positioned elements without z-index, later elements in the source order paint on top. This means you can often control overlap through source order alone, without touching z-index.

A stacking context is a three-dimensional conceptualization of HTML elements along the z-axis. Every stacking context has a single root element, and the z-index of child elements is only meaningful within that context. The root HTML element creates the initial stacking context. New stacking contexts are created by: elements with position and a z-index value other than auto, elements with opacity less than 1, elements with transform, filter, perspective, clip-path, or mask properties, flex/grid children with z-index other than auto, and elements with isolation: isolate.

The critical concept is that stacking contexts are hierarchical and isolated. An element inside one stacking context can never appear between elements in a sibling stacking context, regardless of z-index values. If element A creates a stacking context with z-index: 1, and element B is a sibling with z-index: 2, then every child of A - even one with z-index: 9999 - will appear behind B. This is the root cause of most z-index frustrations.

The isolation property provides a clean way to create a new stacking context without side effects. Setting isolation: isolate on a container creates a stacking context that contains all z-index values within its children, preventing them from interfering with z-index values outside the container. This is the recommended approach for component-based architectures where you want to ensure a component's internal z-index values do not leak out and conflict with the rest of the page.

Code examples

Basic Z-Index Stacking

<style>
  .stack-demo {
    position: relative;
    height: 200px;
    margin: 20px;
  }
  .layer {
    position: absolute;
    width: 180px;
    height: 120px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: white;
    font-weight: bold;
  }
  .red {
    background: #ef4444;
    top: 0;
    left: 0;
    z-index: 1;
  }
  .green {
    background: #22c55e;
    top: 30px;
    left: 60px;
    z-index: 3;
  }
  .blue {
    background: #3b82f6;
    top: 60px;
    left: 120px;
    z-index: 2;
  }
</style>

<div class="stack-demo">
  <div class="layer red">z-index: 1</div>
  <div class="layer green">z-index: 3 (top)</div>
  <div class="layer blue">z-index: 2</div>
</div>

Green (z-index: 3) appears in front of blue (2), which appears in front of red (1). All three are in the same stacking context (the stack-demo parent), so their z-index values are directly comparable.

Stacking Context Isolation Problem

<style>
  .parent-a {
    position: relative;
    z-index: 1; /* Creates stacking context */
    background: #fef3c7;
    padding: 20px;
    margin: 10px;
  }
  .parent-b {
    position: relative;
    z-index: 2; /* Higher than parent-a */
    background: #dbeafe;
    padding: 20px;
    margin: 10px;
    margin-top: -30px; /* Overlap with parent-a */
  }
  .child-high {
    position: relative;
    z-index: 9999; /* Very high, but trapped in parent-a's context */
    background: #ef4444;
    color: white;
    padding: 10px;
  }
  .child-low {
    position: relative;
    z-index: 1;
    background: #3b82f6;
    color: white;
    padding: 10px;
  }
</style>

<div class="parent-a">
  Parent A (z-index: 1)
  <div class="child-high">Child z-index: 9999 (still behind Parent B!)</div>
</div>
<div class="parent-b">
  Parent B (z-index: 2)
  <div class="child-low">Child z-index: 1</div>
</div>

Even though the child in Parent A has z-index: 9999, it cannot escape Parent A's stacking context (z-index: 1). Parent B (z-index: 2) and all its children will always paint in front of Parent A and all its children.

Using isolation: isolate

<style>
  .component {
    isolation: isolate; /* Contains z-index within */
    position: relative;
    padding: 20px;
    margin: 20px;
    border: 1px solid #e2e8f0;
  }
  .tooltip {
    position: absolute;
    top: -10px;
    left: 50%;
    transform: translateX(-50%);
    background: #1e293b;
    color: white;
    padding: 6px 12px;
    border-radius: 4px;
    z-index: 10; /* Only relevant within this component */
    white-space: nowrap;
    font-size: 14px;
  }
  .dropdown {
    position: absolute;
    top: 100%;
    left: 0;
    background: white;
    border: 1px solid #e2e8f0;
    padding: 8px 0;
    z-index: 10; /* Same value, no conflict */
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    min-width: 150px;
  }
</style>

<div class="component">
  <div class="tooltip">Tooltip (z-index: 10)</div>
  Component A
</div>

<div class="component">
  Component B
  <div class="dropdown">
    <div>Option 1</div>
    <div>Option 2</div>
  </div>
</div>

Each component uses isolation: isolate to create its own stacking context. This ensures that z-index values inside one component do not interfere with another component's z-index values. Both use z-index: 10 without conflict.

Properties That Create Stacking Contexts

<style>
  .demo { position: relative; margin: 10px; padding: 16px; }

  /* These all create new stacking contexts: */
  .ctx-zindex {
    position: relative;
    z-index: 0; /* position + z-index (not auto) */
    background: #dbeafe;
  }
  .ctx-opacity {
    opacity: 0.99; /* opacity < 1 */
    background: #dcfce7;
  }
  .ctx-transform {
    transform: translateZ(0); /* Any transform value */
    background: #fef3c7;
  }
  .ctx-filter {
    filter: blur(0px); /* Any filter value */
    background: #fce7f3;
  }
  .ctx-isolation {
    isolation: isolate; /* Explicit stacking context */
    background: #f3e8ff;
  }
  .ctx-flex-child {
    /* Flex/grid child with z-index */
    z-index: 0;
    background: #ecfdf5;
  }

  .inner {
    position: absolute;
    z-index: 999;
    background: #ef4444;
    color: white;
    padding: 4px 8px;
    top: -8px;
    right: -8px;
  }
</style>

<!-- Each of these containers creates a stacking context,
     trapping the .inner element's z-index within -->
<div class="demo ctx-zindex">
  z-index: 0 <span class="inner">Trapped</span>
</div>
<div class="demo ctx-opacity">
  opacity: 0.99 <span class="inner">Trapped</span>
</div>
<div class="demo ctx-transform">
  transform <span class="inner">Trapped</span>
</div>

Many CSS properties create stacking contexts as a side effect: z-index on positioned elements, opacity < 1, transform, filter, perspective, clip-path, mask, and isolation: isolate. This means a child's z-index: 999 is trapped within any of these parent contexts.

Key points

Concepts covered

z-index, Stacking Context, Stacking Order, isolation, Painted Order