| title | CSS z-index Not Working — Stacking Context Inversion in Modal Overlays | ||||||
|---|---|---|---|---|---|---|---|
| domain | frontend | ||||||
| tags |
|
||||||
| status | published | ||||||
| created | 2026-07-21 | ||||||
| source | hermes-agent | ||||||
| confidence | 0.95 | ||||||
| domain_expert | |||||||
| verified_date | |||||||
| subdomain | css |
A modal overlay with z-index: 9999 was appearing behind an element with z-index: 100. Setting higher z-index values on the modal had no effect. The modal's backdrop was visible, but the modal content was hidden behind another page element.
/* This didn't work — modal still behind other elements */
.modal-overlay {
position: fixed;
z-index: 9999;
background: rgba(0,0,0,0.5);
}
.modal-content {
position: relative;
z-index: 10000;
}The modal was rendered inside a parent element that had transform: translateX(...) applied (for a slide-in sidebar animation). In CSS, any element with a transform, filter, perspective, clip-path, mask, or isolation property value other than none creates a new stacking context.
Once a new stacking context is created, z-index values are relative to that context, not the document root. The parent's stacking context was lower in the global stacking order than the competing element.
<!-- Parent creates its own stacking context via transform -->
<div class="sidebar" style="transform: translateX(0); position: relative; z-index: 1;">
<div class="modal-overlay" style="position: fixed; z-index: 9999;">
<!-- z-index: 9999 is still INSIDE the sidebar's stacking context -->
<!-- It can never escape above elements OUTSIDE the sidebar -->
</div>
</div>
<div class="page-header" style="position: sticky; z-index: 100;">
<!-- This element is at document-root stacking context level -->
<!-- z-index: 100 here > z-index: 9999 of modal because modal is nested -->
</div>Option 1: Render modals at the document root (recommended)
Move the modal markup outside any stacking-context-creating parent. In React:
// Instead of rendering inside the sidebar component,
// render modals at the root level using a portal:
import { createPortal } from 'react-dom';
function Modal({ children }) {
return createPortal(
<div className="modal-overlay">
<div className="modal-content">{children}</div>
</div>,
document.body // Renders outside any stacking context
);
}Option 2: Change the parent's stacking context approach
If you can't use a portal, remove the stacking-context-creating property from the parent and use an alternative:
/* Instead of transform for positioning: */
.sidebar {
/* BAD: creates stacking context */
/* transform: translateX(0); */
/* GOOD: use position + left instead */
position: fixed;
left: 0;
top: 0;
}Option 3: Ensure the parent's z-index is above competitors
If the modal MUST stay inside the sidebar:
.sidebar {
transform: translateX(0); /* Keeps animation */
position: relative;
z-index: 10001; /* Must be higher than page-header's z-index */
}
.modal-overlay {
position: absolute; /* relative to sidebar */
z-index: 1; /* Only needs to be >0 inside sidebar */
}echo "Lesson: CSS z-index Not Working — Stacking Context Inversi"
wc -l lessons/contrib/css-z-index-stacking-context-modal.mdExpected Output:
Lesson: CSS z-index Not Working — Stacking Context Inversi
# (line count)
When z-index seems broken:
- Is the element
positionset torelative,absolute,fixed, orsticky? (z-index only works on positioned elements) - Does any ancestor have
transform,filter,perspective,clip-path,mask,isolation, orwill-changeset? - Is the element inside a flex/grid container? (These can create stacking contexts too)
- Is there an
opacityvalue < 1 on any ancestor? (This creates a stacking context in some browsers) - Are you comparing z-index across different stacking contexts?
z-index is not a global value. It's relative to the current stacking context. A z-index: 9999 inside a nested stacking context will always appear below a z-index: 1 at the document root level. When z-index isn't working, the culprit is almost always an unexpected stacking context created by transform, filter, or opacity on a parent element.