z-index: 99999 Isn't Working Because of Stacking Contexts
The support ticket says the dropdown renders behind the sticky header. The dropdown has z-index: 99999; the header has z-index: 50. The number is bigger. The number is also irrelevant, because z-index doesn't compare globally - it compares within a stacking context, and your dropdown is sealed inside one.
The mental model: nested folders
A stacking context is like a folder of layers: children are sorted inside it, and then the folder as a whole is sorted among its siblings by the folder's own z-index. A child with z-index 99999 inside a folder at z-index 10 can never rise above a sibling folder at z-index 50 - its five nines only outrank things in its own folder.
<main style="position: relative; z-index: 10"> <!-- folder A -->
<div class="dropdown" style="z-index: 99999">...</div>
</main>
<header style="position: sticky; z-index: 50">...</header> <!-- folder B: wins -->
What creates a context (this list is the whole game)
The classics: positioned element with z-index, and the root element. The surprises - each of these creates a stacking context even with no z-index anywhere:
transform,translate/scale/rotate(any value)opacityless than 1filter,backdrop-filterwill-change: transform(or opacity)contain: paint/layout,content-visibility: autoposition: fixed/sticky(in most engines for sticky)
That transform entry explains the epidemic: someone adds a hover animation or an entrance transition to a card, and every dropdown inside every card silently gets folder-ized. (Transforms also hijack position: fixed descendants - a related but separate trap.)
The debugging routine
// Console snippet: walk up from the trapped element, name every context-creator
let el = $0;
while ((el = el.parentElement)) {
const s = getComputedStyle(el);
const reasons = [
s.zIndex !== 'auto' && s.position !== 'static' && `z-index:${s.zIndex}`,
s.transform !== 'none' && 'transform',
+s.opacity < 1 && `opacity:${s.opacity}`,
s.filter !== 'none' && 'filter',
s.contain.match(/paint|layout|strict|content/) && `contain:${s.contain}`,
].filter(Boolean);
if (reasons.length) console.log(el, reasons.join(', '));
}
The first hit above your element is usually the jailer. Fixes, in order of preference: raise the z-index of the context-creating ancestor (the folder, not the child); restructure so the overlay isn't inside the context (portals, or the popover attribute/dialog, whose top layer sits above all stacking contexts by design - the modern escape hatch); or remove the accidental context-creator.
isolation: the deliberate folder
.widget { isolation: isolate; }
Sometimes you want a context: it caps the widget's internal z-indexes so they can't fight the page (your component's z-index: 10 tooltip stays inside), and it scopes blend modes. isolation creates the context with zero side effects - better than the position: relative; z-index: 0 folk remedy, and it documents intent.
Once the folder model clicks, z-index bugs stop being mysterious and the 99999s can be deleted - my rule is a small scale (1, 10, 50) within components, top layer for overlays, and suspicion aimed at transforms whenever layering misbehaves.
Full-stack web developer sharing practical tutorials and building tools that ship.
Got something on your mind?
My inbox is open - no forms disappearing into the void here.
- Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
- Hire me for a project Have something custom in mind? Let's talk scope and timelines.
- Product support Bought something here? I'll help you get it running.
I usually reply within 1-2 business days.