What is a stacking context, and what properties create one?
Advertisement
🧩 Scenario
Architecture Walkthrough
A Local Scope for z-index
A stacking context is a self-contained scope for stacking order along the z-axis. Every element inside it is stacked relative to its siblings within that context only, and the whole context is then stacked as a single atomic unit inside its parent context.
The consequence people find counterintuitive is that z-index is not a global scale. A value of 9999 means "highest among my siblings in my context," not "highest on the page." If the context that element lives in is itself painted below another context, nothing inside it can rise above that other context's content. There is no escape hatch: a descendant cannot punch out of its ancestor's stacking context.
The root element <html> establishes the root stacking context, and every other one nests inside it, forming a tree that parallels but does not match the DOM tree, since only some elements create contexts.
What Creates One
The list is longer than most people expect, and the non-positioning entries are where the bugs come from.
The positioning cases are: position: relative or absolute with a z-index other than auto; position: fixed or sticky, which always create one regardless of z-index.
The cases with no positioning involved at all are the important ones:
opacityless than1transform,scale,rotate, ortranslateset to anything other thannonefilterorbackdrop-filterother thannoneperspectiveorclip-pathormaskother thannonemix-blend-modeother thannormalisolation: isolatecontain: layout,contain: paint, orcontent-visibilityvalues that imply themwill-changenaming any property that would itself create one- a flex or grid item with a
z-indexother thanauto, even though it is not positioned
That last point is worth noting separately: flex and grid children are the one case where z-index works without any position value, because the flex and grid specs explicitly opt them in.
isolation: isolate deserves a mention as the deliberate tool in this list. It creates a stacking context and does nothing else, no opacity change, no transform, no compositing side effect, which makes it the correct way to intentionally scope z-index inside a component.
The Paint Order Inside a Context
Within a single stacking context, the browser paints in this order, from back to front:
- The background and borders of the element that established the context
- Descendants with negative
z-index, in ascending order - In-flow, non-positioned, block-level descendants
- Non-positioned floated descendants
- In-flow inline-level descendants, including inline text
- Positioned descendants with
z-index: autoorz-index: 0 - Descendants with positive
z-index, in ascending order
Two useful facts fall out of that list. A negative z-index places an element behind its own parent's in-flow content but still in front of the parent's background, which is how a decorative pseudo-element sits behind text without hiding the card colour. And inline text paints above floats and non-positioned blocks, which is why text wraps visibly over a float's background rather than under it.
Debugging Approach
When stacking looks broken, the fix is almost never on the element you are looking at. Walk up its ancestor chain and find the nearest element that created a stacking context, typically by way of opacity, transform, or filter applied for an animation or a hover effect. That ancestor is the one whose z-index needs to change, or whose context-creating property needs to be removed or made conditional.
Key Code Explained
/* Positioned + z-index other than auto -> creates a context */
.card {
position: relative;
z-index: 1;
}
/* position: relative ALONE does NOT create one */
.anchor-only {
position: relative; /* z-index stays auto -> no stacking context */
}
/* No positioning needed: each of these creates a stacking context */
.animated {
opacity: 0.99;
}
.transformed {
transform: translateZ(0);
}
.blurred {
filter: blur(0);
}
.hinted {
will-change: transform;
}
.contained {
contain: paint;
}
/* The deliberate, side-effect-free way to scope z-index */
.component {
isolation: isolate;
}
/* THE CLASSIC BUG ------------------------------------------------- */
.card-a {
transform: translateY(0); /* creates a stacking context, z-index auto */
}
.card-a .dropdown {
position: absolute;
z-index: 9999; /* scoped to .card-a — cannot escape it */
}
.card-b {
position: relative;
z-index: 1; /* .card-b's whole subtree paints above .card-a's */
}
/* Result: the 9999 dropdown renders BEHIND .card-b.
Fix: raise .card-a (the context creator), not the dropdown. */
.card-a:hover,
.card-a:focus-within {
position: relative;
z-index: 10;
}
/* Negative z-index: behind the parent's content, in front of its background */
.card::before {
content: '';
position: absolute;
inset: 0;
z-index: -1;
background: url('texture.png');
}
/* Flex and grid items honour z-index with NO position value */
.grid > .overlap {
z-index: 2;
}
The .card-a block is the example to be able to reproduce from memory. Everything about it looks correct in isolation: the dropdown is absolutely positioned with an enormous z-index, and the sibling card has a modest z-index: 1. The only reason it fails is a transform on .card-a that nobody added for stacking reasons at all, probably a hover lift or an entrance animation. That is why "raise the z-index" is the wrong instinct and "find the ancestor context" is the right one.
The isolation: isolate declaration is the other detail worth carrying. Developers routinely write transform: translateZ(0) or opacity: 0.999 to force a stacking context, both of which have real side effects on compositing and rendering. isolation does exactly that one job and nothing else.
Tradeoffs
| Way to create a stacking context | Side effects | Good choice? |
|---|---|---|
isolation: isolate | None | Yes, the purpose-built option |
position: relative + z-index: 0 | Becomes a positioned ancestor too | Yes, common and clear |
opacity: 0.999 | Forces compositing, subtle colour change | No, a hack |
transform: translateZ(0) | Promotes to its own layer, extra memory | Only when you also want compositing |
contain: paint | Clips overflow, blocks descendants escaping | Only when containment is wanted |
will-change: transform | Persistent layer promotion, memory cost | No, not for this purpose |
What Interviewers Actually Check
- Whether you can define a stacking context as a local scope rather than describing
z-indexas global - Whether you know
position: relativealone is not enough andz-indexmust not beauto - Whether you can name the non-positioning creators, especially
opacity,transform, andfilter - Whether you know the fix for a nesting bug is on the ancestor, not the element
- Whether you know flex and grid items honour
z-indexwithout being positioned
Follow-Up Questions
- Why does a negative
z-indexplace an element behind its parent's content but still in front of the parent's background? - How does
mix-blend-modeinteract with stacking contexts, and why doesisolation: isolateexist partly to control it? - Do stacking contexts and compositing layers refer to the same thing? Where do they diverge?
- How would you architect z-index in a design system so that modals, toasts, and dropdowns never fight, given that contexts can appear anywhere?
- How does the top layer used by
<dialog>and the Popover API sidestep stacking contexts entirely?
Common Candidate Mistakes
- Treating
z-indexas one global scale for the whole document, which makes nested stacking bugs look like browser defects - Assuming
position: relativeby itself creates a stacking context, whenz-indexmust also be something other thanauto - Not knowing that
opacity,transform, andfiltercreate contexts with no positioning involved, which is the origin of most real-world overlay bugs - Escalating the child's
z-indexto ever larger numbers instead of finding and raising the ancestor that created the context - Forgetting that flex and grid items can use
z-indexdirectly, and adding unnecessaryposition: relativeto make it work
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you define a stacking context in one sentence?
- Can you name at least six properties that create one, including several with no positioning?
- Can you explain how a
z-index: 9999element can paint below az-index: 1element? - Can you recite the paint order layers inside a single context?
- Can you debug a stacking bug by locating the ancestor that created the context?
Summary
A stacking context is a local scope for z-axis ordering. Elements inside it stack relative to their siblings within that context, and the entire context is then painted as one atomic unit in its parent context. Because of that nesting, z-index is never global: a descendant cannot paint above anything outside its ancestor's context, no matter how large its value.
Contexts are created by positioned elements with a z-index other than auto, by fixed and sticky unconditionally, and, crucially, by a long list of properties that involve no positioning at all: opacity below 1, any transform, filter, backdrop-filter, clip-path, mask, mix-blend-mode, isolation: isolate, contain: layout or paint, and will-change on any of those. Flex and grid items also honour z-index without being positioned. isolation: isolate is the purpose-built option when you want a context and nothing else.
Inside a context, painting runs from the context element's own background, through negative z-index descendants, in-flow blocks, floats, inline content, positioned auto/0 elements, and finally positive z-index in ascending order. When stacking looks broken, the productive move is not raising the child's z-index but walking up the ancestor chain to find the element whose transform or opacity created the context, and fixing the ordering there.
Does every positioned element create a stacking context?
No. A positioned element creates one only when z-index is not auto. position: relative with z-index: auto does not create a stacking context.
Can a child escape its parent stacking context?
No. A child can never paint above or below anything outside its parent stacking context. The parent whole subtree is stacked as one unit at the parent level.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement