Skip to main content

Z-Index and Stacking Contexts

Understanding the stacking order is one of the most misunderstood areas of CSS. This page explains z-index and the powerful (and often counterintuitive) concept of stacking contexts.

1. What is Z-Index?

The z-index property controls the order elements overlap each other along the Z-axis (depth into / out of the screen). Higher values appear in front.

.tooltip { z-index: 200; position: absolute; }
.modal { z-index: 300; position: fixed; }
.overlay { z-index: 250; position: fixed; }

/* Result (back to front): tooltip → overlay → modal */

[!IMPORTANT] z-index only works on positioned elements (position: relative, absolute, fixed, or sticky) and on flex/grid items. Setting z-index on a static element has zero effect.

2. Default Stacking Order (Without Z-Index)

When no z-index is set, browsers paint elements in this default order (back to front):

  1. Root element background and borders.
  2. Non-positioned elements in source order.
  3. Positioned elements (positionstatic) in source order.

This means a positioned element will always appear above a non-positioned one, even with no z-index set.

3. What Creates a Stacking Context?

A stacking context is a self-contained layer. Elements inside it are stacked relative to each other and move as a group relative to elements outside. A new stacking context is created by:

PropertyValue
positionabsolute, relative, fixed, sticky with any z-index other than auto
opacityAny value less than 1
transformAny value other than none
filterAny value other than none
will-changeAny property that creates it
isolationisolate
mix-blend-modeAny value other than normal
containlayout, paint, strict, or content
display: flex/grid childrenWith z-index other than auto

4. The Classic z-index: 9999 Trap

This is the most common CSS confusion:

<div class="parent-a"> <!-- Creates stacking context via opacity: 0.99 -->
<div class="child" style="z-index: 9999">I am HUGE z-index!</div>
</div>
<div class="parent-b">
<div class="sibling" style="z-index: 1">I'm small...</div>
</div>
.parent-a { opacity: 0.99; } /* Accidentally creates a stacking context! */

The sibling with z-index: 1 appears ABOVE the child with z-index: 9999 because the entire .parent-a stacking context competes with .parent-b as a unit. Within .parent-a, the child's z-index: 9999 only matters relative to other children of .parent-a.

The Fix: Isolate with isolation: isolate

.create-new-context {
isolation: isolate; /* Explicitly creates a stacking context without side effects */
}

5. Z-Index Scale System

Instead of arbitrary values, use a named z-index scale to stay organized:

:root {
--z-below: -1;
--z-base: 0;
--z-raised: 10;
--z-dropdown: 100;
--z-sticky: 200;
--z-overlay: 300;
--z-modal: 400;
--z-toast: 500;
}

.sticky-header { position: sticky; top: 0; z-index: var(--z-sticky); }
.modal-overlay { position: fixed; inset: 0; z-index: var(--z-overlay); }
.modal-dialog { position: fixed; z-index: var(--z-modal); }
.toast { position: fixed; z-index: var(--z-toast); }

6. Debugging Stacking Issues

Step-by-step process:

  1. Open DevTools → Elements panel.
  2. Select the element that isn't appearing correctly.
  3. In the Styles panel, look for z-index and position.
  4. Walk up the DOM tree — check each parent for properties that create a stacking context (opacity < 1, transform, filter).
  5. Use Chrome DevTools → Layers panel (menu: More tools → Layers) to see a 3D visualization of all stacking contexts on the page.

7. WordPress-Specific Z-Index Issues

Common conflicts in GeneratePress and Bricks:

ProblemCauseFix
Dropdown menu appears behind sectionsGP header z-index too low#site-header { z-index: 200; }
Bricks popup behind other elementsStacking context on a parent sectionAdd isolation: isolate to offending parent
WooCommerce cart dropdown clippedOverflow: hidden on containerRemove overflow: hidden or change to overflow: visible