Debugging CSS Z-Index: Stacking Contexts Demystified
My modal had z-index: 9999. It was still behind the navigation bar. I tried z-index: 999999. Still behind. I tried z-index: 99999999 !important. Nothing.
I spent an hour increasing numbers before finally learning what was actually happening. The problem wasn't the number. The problem was stacking contexts.
Why Bigger Numbers Don't Help
Z-index only compares elements within the same stacking context. Think of it like folders in Photoshop.
If Folder A is above Folder B, everything in Folder A is above everything in Folder B—regardless of layer order inside those folders. A layer with z-index 9999 inside Folder B will still be below a layer with z-index 1 inside Folder A.
That's what was happening to my modal. It was trapped inside a container that had created its own stacking context, and that container was below the nav bar.
What Creates Stacking Contexts
Here's where it gets tricky. Many CSS properties create new stacking contexts, often unexpectedly:
position: relative/absolute/fixedwith any z-index valueopacityless than 1 (even 0.99!)transform(scale, rotate, translate)filter(blur, contrast, etc.)will-change- Flex/grid items with z-index set
.header {
position: relative;
z-index: 10;
}
.content {
opacity: 0.99; /* Creates stacking context! */
position: relative;
z-index: 1;
}
.modal {
position: absolute;
z-index: 9999; /* Trapped inside .content */
/* Will NEVER appear above .header */
}
The modal's z-index only matters compared to siblings inside .content. The whole .content block (z-index: 1) is below .header (z-index: 10), so the modal is stuck.
How to Fix It
Option 1: Move the Element
The cleanest solution: move your modal to be a direct child of <body>. In React, use a Portal. In Vue, use Teleport. Now it's in the root stacking context where z-index works as expected.
Option 2: Remove the Context
Find which parent is creating the trap. Inspect ancestors for transform, opacity, or filter. Sometimes these are added for performance (transform: translate3d(0,0,0) for hardware acceleration). If it's not essential, removing it dissolves the stacking context.
Option 3: DevTools Layers Panel
Chrome DevTools has a "Layers" panel (More tools → Layers). It shows your page in 3D—you can literally rotate and see which elements are stacking where, and why. It often labels "Stacking context created by..." which points directly to the culprit.
Manage Z-Index at Scale
Random numbers like 10, 50, 9999 scattered across files become unmaintainable. Use CSS variables to define a system:
:root {
--z-dropdown: 100;
--z-sticky-header: 200;
--z-modal-backdrop: 300;
--z-modal: 400;
--z-toast: 500;
}
.modal {
z-index: var(--z-modal);
}
Now all your layering logic is in one place. Need to insert something between header and modal? Change numbers once, not in fifty files.
Quick Debugging Steps
- Check if the element has
positionset (z-index needs it) - Find which ancestor creates a stacking context
- Either move the element out or adjust the ancestor
- Use the Layers panel if you're stuck
Z-index issues are almost never browser bugs. They're misunderstandings of how CSS stacking actually works. Once you understand stacking contexts, you stop guessing numbers and start solving problems.