CSS — z-index has no effect
Diagnose z-index failures caused by stacking contexts, parent stacking order, and transform or opacity, then fix the layout instead of escalating numbers.
Symptoms
- An element with z-index: 9999 still renders below another element
- The overlay works on one page but not another with the same CSS
- Changing z-index on a child does nothing
- A modal is clipped or hidden behind a header or a transformed container
Possible causes
- An ancestor creates a stacking context with transform, filter, opacity, isolation, or position plus z-index
- The element and its competitor are in different stacking contexts, so their z-index values are not compared directly
- The parent has a lower z-index than the competing parent, capping the child
- The element is positioned static, so z-index does not apply
- A flex or grid item creates a stacking context through z-index even without position
Diagnosis steps
Inspect the computed position and z-index
DevTools Elements → Computed → filter: position, z-indexInterpretation
z-index applies to positioned elements and flex/grid items. A static element with z-index is ignored.
Walk up the ancestor chain looking for stacking-context triggers
DevTools Elements → check transform, filter, opacity, isolation, will-change, containInterpretation
The first ancestor that creates a stacking context becomes the comparison boundary for its descendants.
Test by temporarily removing candidate triggers
element.style.transform = 'none'; element.style.opacity = '1';Interpretation
If the layering changes, the removed property created the relevant stacking context.
Root cause
The element's z-index is being compared only within its local stacking context. An ancestor with a lower stacking order, or a new context created by transform or opacity, places the whole subtree below the competitor regardless of the child's z-index.
Fix
Fix the stacking context rather than increasing numbers: move the overlay to a top-level portal, raise the ancestor's stacking order, remove an unnecessary transform, or use isolation deliberately. Keep a small, documented z-index scale for layers.
Risk note
Raising z-index to enormous values creates an arms race and still fails when the competing element is inside a higher stacking context.
Verify the fix
Test the overlay against every major layout region (header, sidebar, transformed card, sticky footer) at desktop and mobile widths. Confirm the element is positioned, the intended stacking context is documented, and no ancestor caps it. Check that focus and pointer events reach the overlay.
z-index is local, not global
An element with z-index: 9999 is not automatically above everything on the page. z-index orders elements within a stacking context. An ancestor that creates a new context—through transform, filter, opacity below 1, isolation: isolate, or position plus z-index—becomes the boundary. A child cannot escape its ancestor’s stacking order, no matter how large its z-index.
.card {
transform: translateZ(0); /* creates a stacking context */
}
.card .badge {
position: absolute;
z-index: 9999; /* still below elements outside .card's context */
}
Find the context boundary
In DevTools, inspect the element’s computed position and z-index, then walk up the tree. The first ancestor with a transform, filter, opacity, isolation, or a positioned z-index is the boundary. Temporarily removing the property is a quick confirmation. Also check whether the element is a flex or grid item: a z-index on such an item creates a stacking context even without positioning.
Fix the structure
For modals and tooltips, render at the document root through a portal rather than fighting ancestor contexts. For layered UI, define a small z-index scale (base, dropdown, sticky, modal, toast) and apply it to the layer owners, not every child. Use isolation: isolate when you intentionally want a local context. Do not keep increasing numbers; it hides the structural problem and fails again when a new transformed container appears.
Verify across layouts
Test the overlay against the header, sidebar, transformed cards, and sticky elements at desktop and mobile widths. Confirm the element is positioned, the intended stacking context is documented, and pointer events and focus reach the overlay. A visual fix that breaks keyboard access is not complete.