← Back to the library
FrontendApplied · 6 min

A modal or dropdown hides behind other content despite a huge z-index

“A dropdown with z-index: 9999 renders under a card next to it, a modal appears behind the sticky header, or a menu is cut off at the edge of a scroll container.”

THE PRINCIPLE

An element can never stack above its stacking-context ancestor's siblings, and a transformed or filtered ancestor becomes the containing block even for position: fixed. Overlays belong in the top layer or at the document root.

FIRST MOVES

  1. In DevTools, walk up from the overlay and note each ancestor that creates a stacking context (transform, opacity < 1, filter, will-change, position fixed/sticky, isolation, contain, z-index on positioned or flex/grid items).
  2. Check for overflow: hidden, auto, or clip on an ancestor; it clips absolutely positioned descendants whose containing block is inside it.
  3. If position: fixed is offset or clipped, look for an ancestor with transform, filter, backdrop-filter, or contain: it became the containing block.
  4. Move modals to <dialog>.showModal() and menus or tooltips to the popover attribute: both render in the top layer, above every stacking context and outside ancestor clipping.
  5. Where the top layer does not fit, portal the overlay to the end of <body> and keep a small, named z-index scale for the rest.

TOOLS: THEN → NOW

Escalating z-index values (999, 9999, 2147483647)Top layer via <dialog> and the Popover API, plus a documented z-index scale
Portals plus JavaScript positioning (Popper, Floating UI)Still valid; CSS anchor positioning can replace the JS where your browser support matrix allows

PATTERN SNAPSHOT

<button popovertarget="account-menu">Account</button>
<!-- Top layer: unaffected by ancestor z-index, transform, or overflow. -->
<div id="account-menu" popover>
  <a href="/settings">Settings</a>
  <button popovertarget="account-menu" popovertargetaction="hide">Close</button>
</div>

<style>
  /* For the rest of the page: isolate components so their z-index stays local. */
  .card { isolation: isolate; }
  :root { --z-sticky: 10; --z-toast: 20; }
</style>

CLOSE THE AI. EXPLAIN THIS.

A child with z-index: 100 renders below a sibling of its parent that has z-index: 2. What has to be true about the parent, and what are two ways out?

HOW IT WORKS UNDERNEATH

SOURCES

Guide reviewed