What is z-index — and why 9999 doesn't help

Familiar scene: your dropdown hides under the header. You give it z-index: 100. Nothing. You try 9999. Nothing. You try 999999 — same result.
Here's the trick: your 9999 never argued with the header at all. It lost before the argument started, because some parent further up the tree accidentally created a stacking context — and dragged your whole dropdown down with it. Let's unpack how that works.
z-index isn't "height", it's a place in line
By default the browser paints elements in the order they appear in the HTML: later in the code means on top on screen. z-index lets you step in and set a number in that paint order by hand.
Two things break expectations immediately:
z-indexonly works on positioned elements — ones whosepositionisn'tstatic(sorelative,absolute,fixedorsticky). Exception: direct children of flex and grid containers, wherez-indexworks withoutposition;- the numbers are not compared globally. And that's where the pain lives.
Why 9999 loses to 1
Picture stacks of transparent plates on a table. Inside a stack you can move a plate up and down as much as you like. But if your entire stack sits under the neighbouring one, the top plate in yours is still below. It rose to the top of its stack — and the stacks already settled things between themselves.
In CSS that stack is called a stacking context. One rule:
z-indexis only compared between siblings inside the same stacking context. From outside, the whole context acts as a single element.
So if your modal's parent sits under the header, no number inside will help. The argument isn't happening at your level.
What creates a stacking context (the list worth knowing)
The sneaky part: a context gets created by a pile of properties people set for completely unrelated reasons. The main ones, per the MDN docs:
position: relativeorabsolutetogether with az-indexother thanauto;position: fixedorsticky— always, even withoutz-index;opacityless than 1 — yes,opacity: 0.99counts;transform,filter,backdrop-filterwith any non-initial value;will-changenaming a property that would itself create a context;contain: layout,contain: paint,contain: strict,contain: content;container-type: sizeorinline-size— yes, turning on container queries creates one too;isolation: isolate— the only entry that exists for this on purpose.
Now "but I didn't touch anything" makes sense. A designer asked for a soft animation — in came transform. transform brought a context. The context locked your modal inside a block that itself sits third from the bottom.
How to actually fix it
Step 1. Find the guilty parent. Open DevTools, select your element and walk up the parent chain looking for transform, opacity, filter, position: fixed. Chrome has a direct hint: the Layers tab shows what's nested in what. Once you've found it, you have options.
Step 2. Raise the context, not the element. If the modal is locked inside a card, the card is what needs the z-index — not the modal. That's often the whole fix.
Step 3. Or move the element out of that parent. Modals and tooltips are almost always better rendered at the end of body rather than where they logically "belong". In React, portals do exactly that — they move the node elsewhere in the DOM.
Step 4. Keep things tidy up front. The isolation: isolate property creates a context deliberately: inner z-index values stop leaking into the rest of the page. Handy on a card or widget so other people's 9999 can't reach in. Supported in every browser since 2014–2015.
How to skip the argument entirely
If the element is a modal or a popup by nature, the browser has a separate floor for it: the top layer. It sits above the whole page, and you get in not by having the biggest number, but by the browser putting you there.
What lands there: an element opened via showModal() on a dialog, an element with popover turned on, and a fullscreen element via the Fullscreen API.
Practical takeaway: instead of a div with position: fixed and z-index: 9999, use a real dialog and open it with showModal(). It stays above everything no matter how many stacking contexts you've created — plus you get Esc-to-close and sane focus behaviour for free.
Short version: don't raise the number — find the stack.
Why doesn't z-index work without position?
Because the standard applies z-index to positioned elements. Add position: relative — nothing moves visually, but z-index starts working. The exception is direct children of flex and grid containers, where it works as is.
Can z-index be negative?
Yes: the element drops behind its parent's background but stays inside its own stacking context. A common trick for decorative backdrops. It still can't escape the context.
What numbers should I use?
Anything sensible and sparse — 1, 10, 100 for interface layers. A "9999 vs 10000" arms race is a sign the problem is contexts, not numbers. If the page misbehaves even without modals, check why your CSS isn't working for the neighbouring pair of causes.
Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.





