How Do z-index and Stacking Contexts Work?
Learn how z-index and CSS stacking contexts really work, why huge z-index values fail, and how to debug layering bugs.
Expected Interview Answer
z-index controls the paint order of positioned elements along the axis perpendicular to the screen, but it only works within a stacking context, which is a self-contained layering group created by certain CSS properties, meaning a high z-index can still be trapped visually behind an element in a different, lower stacking context.
A stacking context is formed by the root element, or by any element with position other than static combined with a z-index other than auto, as well as by properties like opacity less than 1, transform, filter, will-change, or isolation: isolate. Once an element creates a stacking context, its own z-index is compared only against its own descendants; the whole context is then treated as a single unit when compared against sibling contexts, using the parent’s z-index. This is why a child with z-index: 9999 can appear behind a sibling subtree that itself has a lower z-index, if that sibling subtree’s context ancestor has a higher stacking order — the child’s huge z-index only wins fights inside its own context, not across contexts. Debugging stacking issues means walking up the DOM to find which ancestors create contexts, not just comparing raw z-index numbers.
- Explains why a “z-index: 9999” fix sometimes does nothing
- Clarifies that stacking contexts nest and isolate z-index comparisons
- Helps debug modal/dropdown/tooltip layering bugs systematically
- Prevents unnecessary global z-index escalation wars in a codebase
AI Mentor Explanation
z-index within one team’s substitution bench works fine — a higher-ranked reserve always gets picked over a lower one on that same bench. But a stacking context is like separate benches for separate teams: the highest-ranked reserve on Team B’s bench cannot simply outrank Team A’s captain, because the two benches are only compared as whole units by team ranking first. So a bench member’s individual rank number never crosses into a rival bench’s overall standing. That nested, context-first comparison is exactly how z-index behaves across stacking contexts.
Step-by-Step Explanation
Step 1
Identify what creates a stacking context
position + z-index != auto, opacity < 1, transform, filter, will-change, or isolation: isolate all create new contexts.
Step 2
Compare within the same context first
z-index values only resolve paint order among siblings inside the same stacking context.
Step 3
Treat a context as a single unit at the parent level
When compared against sibling contexts, the whole subtree is ranked by its context-creating ancestor's z-index.
Step 4
Debug by walking up the DOM
A stuck z-index bug usually means an ancestor unexpectedly created a lower-ranked stacking context.
What Interviewer Expects
- Correct list of properties that trigger a new stacking context
- Clear explanation that z-index comparisons are scoped to a stacking context
- Ability to explain why “z-index: 9999” sometimes fails to fix a layering bug
- A systematic debugging approach (walk ancestors, find context creators)
Common Mistakes
- Assuming z-index is a single global ranking across the whole page
- Not realizing transform or opacity < 1 silently creates a new stacking context
- Escalating z-index values arbitrarily (999, 9999, 99999) instead of diagnosing the real ancestor
- Forgetting that stacking context comparisons happen at the parent context boundary
Best Answer (HR Friendly)
“z-index decides which element appears in front when things overlap, but it only competes within its own layering group, called a stacking context. Certain CSS properties like position, opacity, or transform create a new group, so a huge z-index inside one group can still lose to a much smaller z-index in a different, higher-priority group — that is usually why bumping z-index to a huge number does not fix a layering bug.”
Code Example
/* .parent creates a stacking context via opacity */
.parent {
opacity: 0.99;
position: relative;
z-index: 1;
}
/* .child has a huge z-index, but it is scoped INSIDE .parent’s context */
.child {
position: absolute;
z-index: 9999;
}
/* .sibling is outside .parent, and its context (z-index: 2) beats
.parent’s context (z-index: 1) -- so .child stays hidden behind it
no matter how high .child’s z-index goes */
.sibling {
position: relative;
z-index: 2;
}Follow-up Questions
- Which CSS properties besides position create a new stacking context?
- How would you debug a modal that renders behind a dropdown despite a higher z-index?
- What does isolation: isolate do and when would you use it deliberately?
- How do stacking contexts interact with CSS transforms used for animation?
MCQ Practice
1. What primarily determines whether z-index comparisons happen between two elements?
z-index values are only compared directly among elements within the same stacking context.
2. Which property, besides position + z-index, can create a new stacking context?
opacity < 1 (along with transform, filter, will-change, isolation: isolate) creates a new stacking context.
3. Why might a child with z-index: 9999 still render behind a sibling subtree?
The child's z-index only competes within its own context; the parent context's rank governs cross-context comparisons.
Flash Cards
What creates a stacking context? — position + z-index != auto, opacity < 1, transform, filter, will-change, isolation: isolate, and the root element.
Does z-index compare globally across the page? — No — only within the same stacking context; contexts are compared as whole units to their siblings.
Why does raising z-index sometimes not fix a bug? — The element may be trapped inside an ancestor stacking context ranked below a sibling context.
How to debug a stacking bug? — Walk up the DOM to find which ancestors create stacking contexts, not just compare raw z-index numbers.