z-index & Stacking Contexts

When elements overlap, something has to decide which appears on top. z-index controls that order for positioned elements — but it only works within a stacking context, and misunderstanding stacking contexts is why "z-index: 9999 doesn't work" is such a common complaint.

Default stacking order

Try it yourself
Result

Without z-index, later elements in the HTML paint on top of earlier ones. Positioned elements paint above non-positioned ones.

Changing the order with z-index

Try it yourself
Result

Higher z-index values appear in front. It applies to positioned elements (relative, absolute, fixed, sticky) and to flex and grid items. Negative values are allowed and can push an element behind its parent's content.

Stacking contexts

A stacking context is a group whose children are stacked among themselves, and then the whole group is placed as one unit in its parent. A child can never escape its group, no matter how high its z-index:

Try it yourself
Result

The pink child is trapped inside its parent's stacking context, which sits at level 1. The green sibling at level 2 beats the whole group.

What creates a stacking context

  • The root element (<html>).
  • A positioned element with a z-index other than auto.
  • A flex or grid item with a z-index.
  • opacity less than 1.
  • transform, filter, perspective, clip-path, mask or backdrop-filter other than none.
  • position: fixed or sticky.
  • isolation: isolate, mix-blend-mode, will-change with some values, contain: paint.

The surprising ones — like opacity and transform — are usually behind z-index mysteries.

isolation: isolate

Try it yourself
Result

isolation: isolate creates a stacking context without any other side effects. It's perfect for components that use negative z-index decorations: without it, z-index: -1 would slip behind the page background.

Managing z-index in a project

Random values like z-index: 999 and 99999 escalate quickly. Define a small scale once and reuse it:

CSS z-scale.css
:root {
  --z-dropdown: 10;
  --z-sticky: 20;
  --z-overlay: 30;
  --z-modal: 40;
  --z-toast: 50;
}

.site-header { position: sticky; top: 0; z-index: var(--z-sticky); }
.modal { position: fixed; z-index: var(--z-modal); }

Also remember that dialogs and popovers opened through HTML's top layer (lesson 38 of the HTML course) always appear above everything, with no z-index needed.

Debugging overlap

  1. Is the element positioned (or a flex/grid item)? Otherwise z-index does nothing.
  2. Which stacking context is it in? Look up its ancestors for z-index, opacity, transform or filter.
  3. Compare the stacking contexts' levels, not the children's values.

Chromium dev tools include a "Layers" panel, and extensions can visualise stacking contexts.

Try it yourself

Try it yourself
Result