Home/Learn/HTML, CSS & Responsive/Stacking Contexts and z-index

Stacking Contexts and z-index

Advanced
Layout

z-index only compares siblings within the same stacking context. That single rule explains why a z-index of 9999 can still render behind something set to 1.

Overview

Every developer has escalated a z-index to 9999 and watched the element stay behind its neighbour. The explanation is that z-index is not global: certain properties create a stacking context, and inside one, all children are stacked as a unit. A child with z-index 9999 inside a context whose parent is at z-index 1 can never rise above a sibling of that parent at z-index 2. Knowing which properties create a context — several of them innocuous, like transform and opacity below 1 — turns this from mystery into arithmetic.

The Rule

A worked example, because the abstract rule never convinces anyone.

Why 9999 loses to 2
<div class="a">        <!-- z-index: 1, creates a context -->
  <div class="modal">  <!-- z-index: 9999 -->
</div>
<div class="b">        <!-- z-index: 2 -->

.a { position: relative; z-index: 1 }
.modal { position: fixed; z-index: 9999 }
.b { position: relative; z-index: 2 }

/* .modal renders BEHIND .b.

   Because .a created a stacking context, .modal's 9999 only ranks it
   among .a's descendants. The whole of .a — modal included — is then
   placed at level 1, behind .b at level 2. No number inside .a can
   change that. */

/* The fix is never a bigger number. It is one of:
     - render the modal outside .a (a portal to document.body)
     - remove whatever made .a a stacking context
     - raise .a itself                                        */

/* Within one context, the paint order is:
     the element's background, negative z-index, block boxes,
     floats, inline content, z-index: 0/auto, then positive z-index */

What Creates One

The list, including several that surprise people.

transform and opacity create contexts silently
/* Obvious */
position: relative|absolute|sticky  with a z-index other than auto
position: fixed                     (always)

/* Not obvious — these create a context with NO z-index at all */
opacity: 0.99                       /* anything below 1 */
transform: translateZ(0)            /* any transform */
filter: blur(0)                     /* any filter */
will-change: transform
backdrop-filter
isolation: isolate                  /* deliberate, and useful */
mix-blend-mode
contain: paint
/* a flex or grid CHILD with a z-index */

/* This is why a card with a hover transform suddenly clips its own
   dropdown, and why adding a fade animation breaks a tooltip that
   worked yesterday. */

/* isolation: isolate is the deliberate version: create a context on
   purpose so a component's internal z-indexes cannot leak out or be
   interfered with. */
.card { isolation: isolate }

/* DevTools has a Layers panel that visualises the tree, which beats
   guessing when the ancestor is five levels up. */

Managing It

A small scale, and a single owner for the top layer.

A token scale, and the top layer
/* Tokens, not magic numbers scattered through the codebase */
:root {
  --z-base: 0;
  --z-dropdown: 100;
  --z-sticky: 200;
  --z-overlay: 300;
  --z-modal: 400;
  --z-toast: 500;
}
.modal { z-index: var(--z-modal) }

/* Rules that keep it manageable:
     - values in one file, nowhere else
     - components never exceed 10 internally
     - anything above 100 renders at the body level, via a portal
     - never respond to a stacking problem with a bigger number    */

/* The modern escape hatch is the TOP LAYER, which sits above
   everything regardless of z-index or stacking context: */
dialog.showModal()            /* native dialog */
<div popover>                 /* popover attribute */
/* Both also come with a ::backdrop and, for dialog, focus trapping
   and Escape handling. Where support allows, they remove this whole
   category of problem. */

Key Points to Remember

  • 1z-index only compares elements within the same stacking context, which is why a huge value can still lose
  • 2transform, opacity below 1, filter and will-change all create a stacking context with no z-index involved
  • 3The fix for a trapped element is a portal or removing the context, never a larger number
  • 4isolation: isolate deliberately contains a component's z-indexes so they cannot leak
  • 5Native dialog and the popover attribute render in the top layer, above everything, sidestepping stacking entirely

Interview Questions

Sign in to ask Aria
1

Why can an element with z-index: 9999 render behind one with z-index: 2?

Hard
2

Name three properties that create a stacking context without z-index.

Hard
3

What is the top layer, and how does it differ from a high z-index?

Medium

Ask Aria about Stacking Contexts and z-index

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…