z-index and stacking contexts

CSS · Advanced · 12 min

Overlapping menus, modals and tooltips fail in confusing ways until stacking contexts make sense.

When boxes overlap, the browser paints them in a defined order. z-index changes that order, but only within a stacking context.

z-index works on positioned elements (any position except static) and on flex and grid items. Higher values paint on top of lower ones in the same stacking context.

A stacking context is a group that is painted as a unit: its children are layered among themselves, then the whole group is placed in its parent context according to its own z-index. A child can never escape its context. A modal with z-index: 9999 inside a parent context at z-index: 1 still sits below a sibling context at z-index: 2.

Many properties create a new stacking context, often unintentionally:

  • position relative/absolute with a z-index other than auto; position: fixed or sticky always,
  • opacity below 1, transform, filter, backdrop-filter, mix-blend-mode,
  • isolation: isolate, will-change for some properties, contain: paint, container queries' container-type.

Debugging approach: find the nearest ancestors that form stacking contexts and compare their z-index values, not the child's.

Good practice: use a small scale of z-index tokens (dropdown 10, sticky 20, modal 30), use isolation: isolate on components to contain their internal z-indexes, and for modals and popovers use dialog and popover, which render in the top layer above all stacking contexts.

Example

/* <header class="a"><div class="menu">Menu</div></header> <main class="b">...</main> */
.a { position: relative; z-index: 1; }   /* stacking context */
.menu { position: absolute; z-index: 9999; }  /* only 9999 INSIDE .a */
.b { position: relative; z-index: 2; }   /* paints above all of .a, menu included */

/* fix: raise the context, not the child */
.a { z-index: 3; }

/* contain a component's internal layering */
.card { isolation: isolate; }
.card .badge { position: absolute; z-index: 2; }  /* can't leak above other cards' overlays */

:root { --z-dropdown: 10; --z-sticky: 20; --z-modal: 30; }

Remember

  • z-index only compares elements within the same stacking context
  • opacity < 1, transform, filter and more create contexts
  • Fix layering by adjusting the ancestor context
  • dialog and popover use the top layer, above everything

Watch out for

  • Escalating z-index values on a child whose ancestor context is the real problem.

Check yourself

  1. A tooltip has z-index: 1000 but appears under a sibling section. Its parent has opacity: 0.99. Why?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/CSS/Guides/Positioned_layout/Stacking_context