Why z-index Fails
z-index only works on positioned elements (position: relative/absolute/fixed/sticky). On elements with position: static (the default), z-index is completely ignored.
/* ❌ z-index ignored — element is not positioned */
.element {
z-index: 999;
/* missing position! */
}
/* ✅ z-index works */
.element {
position: relative; /* or absolute, fixed, sticky */
z-index: 999;
}
What Creates a Stacking Context
When an element creates a stacking context, its children's z-index values only compete with each other — they can't escape to "beat" elements outside the context, no matter how high the z-index number.
Things that create a stacking context:
opacityless than 1 (evenopacity: 0.99)- Any
transformvalue (eventransform: translateZ(0)) - Any
filtervalue position: fixedorstickyposition: absolute/relative+ z-index other than autowill-changewith certain valuesisolation: isolate
Debugging
// Check DevTools: Elements panel → Computed tab → look for:
// - Any transform on parent elements
// - opacity < 1 on parent elements
// - will-change values
// In console, check parent element computed styles:
const el = document.querySelector('.problem-element');
let parent = el.parentElement;
while (parent) {
const styles = window.getComputedStyle(parent);
if (styles.transform !== 'none' || styles.opacity !== '1') {
console.log('Stacking context creator:', parent, styles.transform, styles.opacity);
}
parent = parent.parentElement;
}
The Fix
// Option 1: Remove the stacking context from the parent
// (if you don't need the transform/opacity)
.parent {
/* remove: transform: translateZ(0); */
/* remove: opacity: 0.99; */
}
// Option 2: Move the element outside the stacking context
// Use JavaScript to append it to document.body for modals/tooltips
// Option 3: Use isolation: isolate deliberately
// to contain stacking contexts intentionally
.card { isolation: isolate; }
// Option 4: Portal pattern (React)
// Render modals/tooltips at the document root level