How Stacking Contexts Work
A stacking context is a self-contained group of elements that get painted together, in a defined order, as a single unit relative to their surroundings. Once you understand that z-index values only ever compete against sibling values inside the same stacking context, a huge category of "why isn't my z-index working" bugs stops being mysterious.
What Creates a New Stacking Context
The root element always forms the outermost stacking context. Beyond that, a long list of CSS properties and values create a new one on the element they're applied to. The most common triggers:
Trigger | Notes |
|---|---|
The root element | <html> always starts a context — the base of the whole stack. |
| The classic, most-taught trigger. Positioning alone is not enough — it needs an explicit z-index too. |
Flex or grid item with | You do not need |
| Any value below |
| Any 2D or 3D transform function, even a no-op like |
| Blurs, drop-shadows, etc. |
| Frosted-glass effects. |
| Used for 3D transform scenes. |
| e.g. |
| Creates a context with no other visual side effect — useful purely for containment. |
| Blend modes need an isolated compositing group to blend correctly. |
| CSS Containment also establishes a stacking context. |
| Historically inconsistent; modern browsers generally treat these as context-creating. |
/* Any of these creates a NEW stacking context on .box */
.box { position: relative; z-index: 0; }
.box { opacity: 0.99; }
.box { transform: translateZ(0); }
.box { filter: blur(0); }
.box { isolation: isolate; }
.box { will-change: transform; }The Core Rule: z-index Only Compares Within a Context
This is the single most important idea on this page: z-index values are only ever compared against sibling elements that belong to the same stacking context. An element with z-index: 9999 cannot climb above an element in a sibling stacking context that has a lower z-index — because they are never compared directly. What gets compared is the context itself, as a whole, against its own siblings.
.card {
position: relative;
z-index: 1; /* .card is now a stacking context */
}
.card .tooltip-button {
position: absolute;
z-index: 9999; /* huge number, but trapped inside .card's context */
}
.sidebar {
position: relative;
z-index: 2; /* .sidebar is also a stacking context */
}
/* Result: .sidebar (z-index: 2) paints above .card (z-index: 1)
as WHOLE UNITS. The button's z-index: 9999 never gets compared
against anything inside .sidebar — it's irrelevant outside .card. */The fix is to stop the parent from trapping the child in a low-value context. Either raise the parent's z-index above the sibling's, or — often cleaner — remove the property that created the unwanted stacking context on the parent in the first place, or restructure so the button isn't nested inside a competing context at all (for example, rendering it in a portal at the top level, as many modal/ tooltip libraries do).
/* Fix option 1: raise the parent context's z-index */
.card { position: relative; z-index: 3; } /* now above .sidebar (2) */
/* Fix option 2: don't give .card its own stacking context at all */
.card { /* no position/z-index/transform here */ }
.card .tooltip-button {
position: fixed; /* escape into the nearest ancestor context instead */
z-index: 9999;
}How Contexts Nest
Stacking contexts form a tree, mirroring the DOM. Inside any single context, its direct children are ordered by a fixed painting algorithm (roughly: background/borders of the context root, then descendants with negative z-index, then normal in-flow content, then descendants with z-index: auto or position: static that themselves create contexts, then positioned descendants with z-index: 0 or auto, then finally descendants with a positive z-index — from lowest to highest). Once an element creates its own context, everything inside it is painted as a single atomic layer at whatever position that context earns among its own siblings.
A context can only be compared to its own siblings — never to a cousin several levels removed in a different branch.
Nesting is unlimited: a context can contain child contexts, which can contain their own child contexts.
Raising a deeply nested element's z-index can never let it escape its ancestor contexts — you have to fix the stacking at the level where the conflict actually happens.