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

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.