Why can z-index: 999999 lose?
Two overlapping elements. One asks for 999999. The other asks for 2. The 2 wins. Nothing is broken.
- Level
- ENGINEER
- Read time
- 09 min
- Experiment
- Available
- Type
- INTERACTIVE
The question
An element with z-index: 999999 is painted underneath an element with z-index: 2. No !important, no JavaScript, no browser bug. Why?
Hypothesis
z-index is not a global depth axis. It orders siblings inside one stacking context, and some innocuous-looking properties quietly create new ones.
Method
Laboratory available
The instrument for this investigation runs in the laboratory, where the controls, the live model and the observation log share one workstation.
Enter the laboratoryTwo panels overlap. Panel A lives inside a wrapper and carries an absurd z-index. Panel B is a plain sibling of that wrapper. Toggle properties on the wrapper and watch which panel survives.
Protocol
- 01Start with a clean wrapper. Panel A (999999) paints above Panel B (2). Expected.
- 02Set
opacity: 0.99on the wrapper. Panel A drops behind Panel B. - 03Undo it. Try
transform: translateZ(0). Same result. - 04Try
filter: blur(0px),will-change: transform,isolation: isolate,contain: paint. - 05Now raise the wrapper's own z-index instead of Panel A's and watch the order restore.
What we observed
Every property in that list produces the same outcome: the wrapper becomes a stacking context, and Panel A's 999999 stops being comparable to Panel B's 2. The number did not shrink, it changed jurisdiction.
Why it happens
Painting is a tree walk, not a global sort. The browser paints the root stacking context, and whenever it meets an element that establishes a new stacking context it paints that element's entire subtree as a single unit before moving on. Descendant z-index values are resolved *inside* that unit and can never escape it.
.wrapper { position: relative; opacity: 0.99; /* ← establishes a stacking context */}.panel-a { position: absolute; z-index: 999999; /* sorted only against siblings in .wrapper */}.panel-b { position: absolute; z-index: 2; /* sorted against .wrapper itself, which has z-index: auto */}Because .wrapper computes to z-index: auto, it is painted in the positioned-elements step at effectively level 0. .panel-b sits at level 2 in the *same* parent context, so it paints later, on top, and takes the entire wrapper subtree, 999999 included, with it.
| Step | What is painted |
|---|---|
| 1 | Background and borders of the element forming the context |
| 2 | Child stacking contexts with negative z-index |
| 3 | In-flow, non-inline-level, non-positioned descendants |
| 4 | Non-positioned floats |
| 5 | In-flow inline-level, non-positioned descendants |
| 6 | Positioned descendants with z-index: auto or 0 |
| 7 | Child stacking contexts with positive z-index, ascending |
Why does opacity create a stacking context at all?
Because the element's subtree has to be composited as one image before the opacity is applied.
Further research
The practical consequence: a modal, dropdown or tooltip can never escape an ancestor stacking context by raising its z-index. The fixes are structural, move the element out of the subtree (a portal), stop the ancestor from creating a context, or raise the ancestor itself.
References
- 01MDNThe stacking context
- 02CSS 2.1. Appendix EElaborate description of stacking contexts
- 03W3CCSS Positioned Layout Level 3