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:
positionrelative/absolute with az-indexother than auto;position: fixedorstickyalways,opacitybelow 1,transform,filter,backdrop-filter,mix-blend-mode,isolation: isolate,will-changefor 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.