The CSS Stacking Context and z-index
A very common question asked in frontend interviews to evaluate practical layout knowledge is:
"Why isn't my
z-indexworking? Explain what a Stacking Context is and how it affects the rendering order of elements."
Most developers know that a higher z-index value brings an element closer to the viewer. However, many encounter a situation where an element with z-index: 9999 is somehow hidden behind an element with z-index: 1.
This is where understanding the Stacking Context becomes crucial.
1. What is a Stacking Context?
The Stacking Context is a three-dimensional conceptualization of HTML elements along an imaginary z-axis relative to the user. Elements occupy this space in order of priority based on element properties.
When a stacking context is formed on an element, that element becomes the root of a new local z-axis for all its children.
The Key Rule
[!IMPORTANT] The
z-indexvalues of child elements only have meaning within their parent's stacking context.
If Element A and Element B are in different stacking contexts, and Element A's stacking context is below Element B's stacking context, then no matter how high you set the z-index of a child inside Element A, it will never appear above Element B.
2. The 7 Layers of Stacking Order (Within a Single Context)
Before discussing how stacking contexts are created, we must understand how a browser orders elements within a single stacking context by default.
Here is the exact stacking hierarchy (from back to front):
The 7 Layers of Stacking Order (Back to Front)
💡 Elements on higher layers will always cover elements on lower layers, regardless of their source code order.
3. How is a Stacking Context Created?
A common misconception is that only position: absolute or position: relative combined with z-index creates a stacking context. While true, many other modern CSS properties trigger a new stacking context.
An element forms a new stacking context if any of the following are true:
- It is the root element (
<html>). - It has
position: absoluteorrelativeAND az-indexvalue other thanauto. - It has
position: fixedorsticky. - It is a child of a Flexbox or Grid container AND has a
z-indexvalue other thanauto. - It has an
opacityvalue less than1. - It has a
transform,filter,perspective,clip-path, ormaskproperty other thannone. - It has
isolation: isolate. - It has a
will-changevalue specifying any property that would trigger a context on its own (liketransformoropacity).
[!WARNING] Applying a seemingly unrelated property like
opacity: 0.99ortransform: scale(1)to a parent element will instantly create a new stacking context, potentially trapping all its children behind other elements on the page!
4. The Classic z-index Trap (Example)
Consider the following HTML and CSS:
<div className="parent-a">
<div className="child-a">Child A (z-index: 9999)</div>
</div>
<div className="parent-b">
<div className="child-b">Child B (z-index: 1)</div>
</div>.parent-a {
position: relative;
z-index: 1; /* Creates Stacking Context A */
}
.child-a {
position: absolute;
z-index: 9999;
}
.parent-b {
position: relative;
z-index: 2; /* Creates Stacking Context B */
}
.child-b {
position: absolute;
z-index: 1;
}What happens here?
Even though child-a has a massive z-index of 9999, it will render behind child-b (which only has a z-index of 1).
Here is a visual map of how the browser evaluates this:
Visual Stacking Hierarchy of the Trap
Child B (z-index: 1 within Context B)
Why does this happen?
Because parent-a has a z-index of 1, and parent-b has a z-index of 2. The browser compares the parents first. Since parent-b is higher than parent-a, everything inside parent-b will render on top of everything inside parent-a. The z-index: 9999 of child-a only means it is on top of other things inside parent-a.
5. How to Debug and Fix Stacking Issues
If your element is trapped behind another, follow these steps to diagnose and repair the bug:
- Check the parent chain: Walk up the DOM tree of the hidden element and look for any parents with
position,opacity,transform,filter, or grid/flex layouts. - Use the
isolationproperty: If you need to intentionally trap thez-indexof child elements to prevent them from bleeding out over other parts of your app, useisolation: isolateon the container. This is cleaner than hacking it withz-index: 1; position: relative. - Use a Portal: In React/Next.js, if you have a Modal or Tooltip that needs to break out of all stacking contexts and render on top of the whole page, use a Portal (
createPortal) to append it directly todocument.bodyor#portal-root.
Key Takeaways
z-indexis not global. It operates locally within its parent's stacking context.- Many CSS properties besides
position(likeopacity,transform, andfilter) create new stacking contexts. - Sibling stacking contexts are compared first. If a parent is "lower" than another parent, its children cannot break out to overlay the higher parent.
- Use
isolation: isolateto purposefully create stacking contexts, and Portals to escape them.