Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

z-index: 99999 Isn't Working Because of Stacking Contexts

C
Carlo Fontanos
· 3 min read

The support ticket says the dropdown renders behind the sticky header. The dropdown has z-index: 99999; the header has z-index: 50. The number is bigger. The number is also irrelevant, because z-index doesn't compare globally - it compares within a stacking context, and your dropdown is sealed inside one.

The mental model: nested folders

A stacking context is like a folder of layers: children are sorted inside it, and then the folder as a whole is sorted among its siblings by the folder's own z-index. A child with z-index 99999 inside a folder at z-index 10 can never rise above a sibling folder at z-index 50 - its five nines only outrank things in its own folder.

<main style="position: relative; z-index: 10">   <!-- folder A -->
    <div class="dropdown" style="z-index: 99999">...</div>
</main>
<header style="position: sticky; z-index: 50">...</header>   <!-- folder B: wins -->

What creates a context (this list is the whole game)

The classics: positioned element with z-index, and the root element. The surprises - each of these creates a stacking context even with no z-index anywhere:

  • transform, translate/scale/rotate (any value)
  • opacity less than 1
  • filter, backdrop-filter
  • will-change: transform (or opacity)
  • contain: paint/layout, content-visibility: auto
  • position: fixed/sticky (in most engines for sticky)

That transform entry explains the epidemic: someone adds a hover animation or an entrance transition to a card, and every dropdown inside every card silently gets folder-ized. (Transforms also hijack position: fixed descendants - a related but separate trap.)

The debugging routine

// Console snippet: walk up from the trapped element, name every context-creator
let el = $0;
while ((el = el.parentElement)) {
    const s = getComputedStyle(el);
    const reasons = [
        s.zIndex !== 'auto' && s.position !== 'static' && `z-index:${s.zIndex}`,
        s.transform !== 'none' && 'transform',
        +s.opacity < 1 && `opacity:${s.opacity}`,
        s.filter !== 'none' && 'filter',
        s.contain.match(/paint|layout|strict|content/) && `contain:${s.contain}`,
    ].filter(Boolean);
    if (reasons.length) console.log(el, reasons.join(', '));
}

The first hit above your element is usually the jailer. Fixes, in order of preference: raise the z-index of the context-creating ancestor (the folder, not the child); restructure so the overlay isn't inside the context (portals, or the popover attribute/dialog, whose top layer sits above all stacking contexts by design - the modern escape hatch); or remove the accidental context-creator.

isolation: the deliberate folder

.widget { isolation: isolate; }

Sometimes you want a context: it caps the widget's internal z-indexes so they can't fight the page (your component's z-index: 10 tooltip stays inside), and it scopes blend modes. isolation creates the context with zero side effects - better than the position: relative; z-index: 0 folk remedy, and it documents intent.

Once the folder model clicks, z-index bugs stop being mysterious and the 99999s can be deleted - my rule is a small scale (1, 10, 50) within components, top layer for overlays, and suspicion aimed at transforms whenever layering misbehaves.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading