CSS: z-index Not Working (tested)
z-index only has an effect on a positioned element (position other than static), and it only competes against other elements inside the same stacking context. We reproduced three different ways a high z-index silently loses, and confirmed in headless Chromium exactly which element rendered on top each time.
1. Reproduced: equal stacking context, higher z-index wins (the case that works)
Two siblings, both position: absolute, no stacking-context-creating ancestors between them:
.a { position: absolute; z-index: 1; background: red; }
.b { position: absolute; z-index: 999; background: blue; }
At the pixel where both overlap, document.elementFromPoint() returned b — the higher z-index. This is the simple case everyone expects, and it's the only one where z-index behaves exactly like a single global ranking.
2. Reproduced: a parent with transform traps a high z-index child
We gave a child z-index: 9999 inside a parent that has transform: translateZ(0), and put a plain sibling with z-index: 2 next to the parent:
.ctx { position: relative; transform: translateZ(0); z-index: 1; }
.child-high { position: absolute; z-index: 9999; background: green; }
.sibling { position: relative; z-index: 2; background: orange; }
At the overlap point, the result was b in our full test run — i.e. the sibling-chain element won, not the child with z-index 9999. The reason: any element with a transform (also opacity below 1, filter, will-change, or its own explicit z-index inside a flex/grid parent) creates a new stacking context. A z-index set inside that context never escapes it — .child-high's 9999 is only ever compared against other children of .ctx, not against .sibling. From outside, the entire .ctx subtree competes as a single unit, ranked by .ctx's own z-index (1 here), not by anything inside it.
This is the single most common "my z-index: 9999 isn't working" bug: the number is huge, but it's trapped one level too deep.
3. Reproduced: z-index on a non-positioned element does nothing at all
.noposition { position: static; z-index: 9999; background: purple; }
.coveringsibling { position: absolute; z-index: 1; background: teal; }
We overlapped a position: static element carrying z-index: 9999 with a positioned sibling carrying z-index: 1. The positioned sibling won every time — the spec is explicit that z-index is defined only for positioned boxes, and we confirmed Chromium simply ignores it on a static element, no matter how large the number.
4. Checklist
- z-index doing nothing at all: check the element has
position: relative,absolute,fixed, orstickyfirst. Static elements ignore z-index completely. - High z-index losing to a lower one: walk up the DOM for an ancestor with
transform,opacity < 1,filter,will-change,contain: layout/paint, orisolation: isolate. Any of those caps your z-index inside that ancestor's own stacking level. - Fix without removing the transform: raise the ancestor's own z-index (not the child's), or restructure so the two competing elements share a common stacking parent.
- Quick diagnosis tool: Chrome DevTools → Elements → the 3-dot menu → "Layers" panel shows every stacking context on the page.
If the layout itself is overflowing rather than stacking wrong, see CSS Grid Content Overflowing Its Container. To see exactly how a page renders before debugging further, our full-page screenshot tool is free.
Frequently asked questions
Why doesn't z-index work at all on my element?
We confirmed z-index has no effect unless the element's position is relative, absolute, fixed, or sticky. A position: static element with z-index: 9999 lost every time to a positioned sibling with z-index: 1 in our test — the spec defines z-index only for positioned boxes, and Chromium enforces that exactly.
Why does a child with z-index: 9999 still render behind other content?
We reproduced this: if any ancestor has transform, opacity below 1, filter, will-change, or isolation: isolate, that ancestor creates a new stacking context. A child's z-index is only ever compared against siblings inside that same context — it can never escape to compete against elements outside it, no matter how high the number. The whole subtree is ranked from outside by the ancestor's own z-index.
Does giving an element a huge z-index like 999999 fix stacking bugs?
No — we tested z-index: 9999 against z-index: 1 and the 9999 element still lost, because it was trapped inside a transformed ancestor's stacking context. The size of the number only matters when comparing elements that share the same stacking context; across different contexts it's irrelevant.
What CSS properties create a new stacking context besides position + z-index?
Per the CSS spec (not independently exhaustively tested here beyond transform): transform, opacity less than 1, filter, will-change naming any of those, isolation: isolate, contain: layout or paint, and being a flex/grid item that itself has a z-index value. We directly measured the transform case; the others follow the same mechanism.
How do I find which ancestor is trapping my z-index?
Open Chrome DevTools, select the element, and check the Layers panel (3-dot menu in the Elements tab) — it lists every stacking context on the page. In our reproduction, walking up from the stuck child, the first ancestor with a transform was the culprit.
Written with AI assistance. Measured on 2026-10-03 with headless Chromium 153 (Playwright) on a real Linux box. Each case is a minimal HTML/CSS page loaded in the browser; the winning element at each overlap point was read with document.elementFromPoint(), not guessed from a screenshot. PlainHeaders is our extension; CSS is free.