Why it matters
z-index: 9999 that still sits under a header with z-index: 10 is one of the oldest CSS puzzles, and the answer is always the same: z-index only competes inside a stacking context. A transform, an opacity below 1, a filter, position: sticky, will-change or contain on an ancestor creates a new context, and everything inside it, whatever its z-index, is painted as one layer at the ancestor's level. DevTools doesn't show these contexts, so the fix is usually more nines.
This debugger renders your HTML and CSS in a sandboxed frame and asks the browser for each element's computed style, so it sees exactly what the browser sees: every stacking context, the property that created it, and every z-index that's ignored because the element isn't positioned. It rebuilds the paint order from the CSS spec's layers (negative z-index, normal flow, z-index auto or 0, positive z-index), then finds overlapping elements where the higher z-index paints underneath and explains which ancestor trapped it and what it was compared with. Pick any two elements to see which paints on top and why.
How it works
- InputThe HTML and CSS that misbehave
- In your browserRendered; every context and paint layer read from the browser
- OutputWhy each z-index wins or loses, and the fix
Where it fits in a designer's workflow
- Plan
- Design
- Build
- Test
- Launch
By the numbers
Show the numbers as a table
| Element | Created by | z-index | Inside | Layers in it |
|---|---|---|---|---|
| header.site-header | position: sticky | 10 | html | 1 |
| nav.menu | opacity: 0.99 | auto | header.site-header | 1 |
| ul.dropdown | position: absolute with z-index: 9999 | 9999 | nav.menu | 0 |
| section.hero | position: relative with z-index: 20 | 20 | html | 1 |
| div.hero-bg | position: absolute with z-index: -1 | -1 | section.hero | 0 |
| div.card | transform | auto | html | 1 |
| div.tooltip | position: absolute with z-index: 100 | 100 | div.card | 0 |
| div.promo | position: relative with z-index: 1 | 1 | html | 0 |
How to use it
- Paste the HTML and CSS that misbehave.
- Read the red findings: each names the trapping ancestor and the fix.
- Pick two elements to compare.
- Check the context tree for unexpected contexts.
FAQ
Why doesn't a bigger z-index win?
z-index only orders elements within the same stacking context. If an ancestor creates a context, the element's z-index is compared only with its siblings inside it, and the whole context is placed by the ancestor's own z-index.
What creates a stacking context?
The root element; position fixed or sticky; position relative or absolute with a z-index other than auto; flex and grid items with a z-index; opacity below 1; transform, translate, scale or rotate; filter and backdrop-filter; clip-path and masks; mix-blend-mode; isolation: isolate; will-change of those properties; contain: layout or paint; and container queries' size containment.
Why is my z-index ignored?
z-index needs the element to be positioned (relative, absolute, fixed or sticky) or to be a flex or grid item. On a static block or inline element it does nothing.
How do I get a dropdown above everything?
Give the trapping ancestor a higher z-index, remove the property that creates its context, or render the dropdown outside it: a portal at the end of body, or the popover attribute, which puts it in the top layer.
Does it run my JavaScript?
No. The HTML is rendered in a sandboxed frame with scripts removed, so state added by scripts isn't seen.