DEV Community

Cover image for Why your z-index doesn't work: my debugger now points to the exact line
Antonio
Antonio

Posted on

Why your z-index doesn't work: my debugger now points to the exact line

The same evening I shipped this, Chrome opened "New in DevTools" and proudly announced DevTools for agents: AI that reads your CSS rules. Thanks Google, I'll take it as a sign I'm on the right track. Their version is for AI agents. Mine is for a 53-year-old guy pressing F6πŸ˜‚πŸ˜‚πŸ˜‚πŸ˜‚πŸ˜‚πŸ˜‚.

We've all done it. A popup stays behind the header, so you write z-index: 999. Then 9999. Then 99999. Nothing changes.

The problem is almost never the element you're fixing. It's one of its ancestors.

The invisible box

When an element has a transform, an opacity below 1, a filter, or a position with a z-index, it creates a stacking context. Think of it as a closed box: its children can only stack inside the box. A child with z-index: 99999 is still only as high as the box itself.

DevTools won't tell you which ancestor is the box. You have to climb the tree by hand.

The idea came from a comment

After my last post about BeeLadybug's CSS hunt, a reader suggested in the comments: scan the ancestors with getComputedStyle, flag whatever creates a stacking context, then group it by cause, like the click-blocker lines. He also warned that will-change is often set on purpose for animations, so it should be a hint, not a verdict.

So that's exactly what 0.5.4 does.

What it looks like

On the demo page:

div.hero creates stacking context (transform) - z-index of 3 children is trapped
Enter fullscreen mode Exit fullscreen mode

Click the line and it lists span.b1, span.b2, span.b3.

will-change stays a grey info line, with the z-index note on the same line:

div.layer-hint will-change: opacity β€” z-index of 1 child stays in this layer
Enter fullscreen mode Exit fullscreen mode

Then I ran it on my own site

I tested it on my live HTML5 games portal. Logged in, it flagged the WordPress admin bar blocking clicks (only I see that). In incognito, only this was left:

div.entry-content creates stacking context (z-index) - z-index of 1 child is trapped
Enter fullscreen mode Exit fullscreen mode

And the INSPECT section went one step further. It showed the rule responsible, inside my LiteSpeed-minified CSS:

.site-header, .main-navigation, .entry-content, .griglia-giochi-spc {
  position: relative !important;
}
Enter fullscreen mode Exit fullscreen mode

One !important rule from my own custom CSS, creating four boxes at once. It's not a bug today, since each box has a single child. But the day I add a dropdown that must cover the header, I already know where to look.

That's the point: not "something is wrong", but "this line caused it".

Try it (F12, paste in the console)

import('https://cdn.jsdelivr.net/npm/bee-ladybug@0.5.4/src/index.js')
  .then(({ BeeLadybugCore, WebDOMAdapter, CSSAdapter }) => {
    const bee = new BeeLadybugCore();
    new WebDOMAdapter(bee).attach(document.body);
    new CSSAdapter(bee).attach();
  });
Enter fullscreen mode Exit fullscreen mode

Press F6 for the CSS hunt, S to scan the page. Zero dependencies, no extension.

Next time your z-index "doesn't work", let the ladybug find the box. 🐞

Top comments (2)

Collapse
 
johnnylemonny profile image
𝗝𝗼𝗡𝗻 •

The "closed box" analogy is the best way to describe stacking context traps.
Manually climbing up the DOM tree in DevTools checking parent nodes for stray transform, filter, or opacity values is one of the most tedious rituals in CSS debugging. Pointing directly to the culprit rule in the stylesheets and listing which children are trapped - while treating will-change as an informational hint rather than a false positive, is a massive time-saver. Love that it runs straight from the console with zero setup!

Collapse
 
antonioprosperi2svg profile image
Antonio •

Thank you! "Closed box" was the only way I could explain it to myself too: z-index looks right, but the parent has already sealed it inside. Walking up the DOM by hand is exactly the ritual I wanted to skip, and will-change felt unfair to flag as a bug, so it stays a hint. Glad the console-only setup clicked. If you try it on a real project and it misses something, I'd love to hear about it.