Skip to content
CSS Lab · LAB-02stacking context / painting order

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 laboratory

Two 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

  1. 01Start with a clean wrapper. Panel A (999999) paints above Panel B (2). Expected.
  2. 02Set opacity: 0.99 on the wrapper. Panel A drops behind Panel B.
  3. 03Undo it. Try transform: translateZ(0). Same result.
  4. 04Try filter: blur(0px), will-change: transform, isolation: isolate, contain: paint.
  5. 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.

example.csscss
.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 */}
The minimal reproduction

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.

StepWhat is painted
1Background and borders of the element forming the context
2Child stacking contexts with negative z-index
3In-flow, non-inline-level, non-positioned descendants
4Non-positioned floats
5In-flow inline-level, non-positioned descendants
6Positioned descendants with z-index: auto or 0
7Child stacking contexts with positive z-index, ascending
Simplified paint order inside one stacking context (CSS 2.1, Appendix E)
The Why MachineDEPTH 1 / 4

Why does opacity create a stacking context at all?

  1. 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

  1. 01MDNThe stacking context
  2. 02CSS 2.1. Appendix EElaborate description of stacking contexts
  3. 03W3CCSS Positioned Layout Level 3