Why is my z-index not working?

Short answer

z-index only applies to positioned elements and flex/grid items. If it is set and still ignored, an ancestor has created a stacking context that the element cannot escape.

#Cause 1: the element is static

css
.tooltip {
  z-index: 999;        /* ignored */
}

z-index needs position: relative, absolute, fixed or sticky — or the element must be a flex or grid item.

css
.tooltip {
  position: relative;
  z-index: 999;        /* now it applies */
}

#Cause 2: a stacking context is trapping it

This is the one that wastes an afternoon. A child can never escape its parent's stacking context, no matter how large its z-index is:

css
.card   { position: relative; z-index: 1; }
.modal  { position: fixed;    z-index: 9999; }   /* inside .card */

The modal sits above everything inside .card, but .card as a whole is still at layer 1. Any sibling with z-index: 2 covers it.

#What creates a stacking context

Far more than people expect:

  • position other than static, with a z-index that is not auto
  • opacity less than 1
  • transform, filter, backdrop-filter, perspective
  • will-change naming any of the above
  • isolation: isolate
  • mix-blend-mode other than normal
  • contain: paint

An opacity: 0.99 added for a fade is enough to trap every descendant.

#How to debug it

In Chrome DevTools, the Layers panel shows the real stacking order. Or walk up the DOM from your element and check each ancestor for the properties above.

#How to fix it

Move the element out of the trapping ancestor — usually to a direct child of <body>. This is why modal libraries portal their markup to the end of the document.

Remove the context if it was accidental, such as an unnecessary opacity: 0.999.

Use <dialog> for modals. It renders in the browser's top layer, above every stacking context, and gives you focus trapping and Escape-to-close for free.