CSSz-indexStacking ContextFrontend

Why z-index Fails: Understanding CSS Stacking Context

Understand why z-index: 9999 can still appear behind another element by learning stacking contexts, parent boundaries, transform, opacity, isolation, and portal-style fixes.

·Updated ·7 min read·Counting...
Why z-index Fails: Understanding CSS Stacking Context

When z-index: 9999 still appears behind another element, the browser is usually not broken. You are probably comparing elements in different stacking contexts. z-index is not a global ranking system for the whole page. It is meaningful inside the current stacking context.

What a stacking context is

A stacking context is an independent layering world. Children are stacked inside that context, and then the entire context is compared as one unit in its parent context.

The key rule:

  • Elements in the same stacking context can compare z-index directly.
  • Elements in different contexts are first limited by their parent contexts.

If a child has z-index: 9999 inside a parent that loses to a sibling context, the child still loses.

Common stacking context triggers

Common conditions that create stacking contexts include:

  • The root html element.
  • position: absolute or relative with non-auto z-index.
  • position: fixed or sticky.
  • opacity < 1.
  • transform.
  • filter.
  • clip-path.
  • perspective.
  • isolation: isolate.
  • Some flex or grid items with non-auto z-index.

This is why a small animation or transparency change can unexpectedly change layer behavior.

Why z-index looks ignored

Consider:

<div class="card">
  <div class="tooltip">tooltip</div>
</div>

<div class="modal">modal</div>
.card {
  position: relative;
  z-index: 1;
}

.tooltip {
  position: absolute;
  z-index: 9999;
}

.modal {
  position: relative;
  z-index: 2;
}

The tooltip may still appear behind the modal. The parent comparison is:

card (1) < modal (2)

The tooltip's 9999 only wins inside .card. It does not jump out and beat .modal.

transform and opacity surprises

These properties are common sources of accidental contexts:

.panel {
  transform: translateY(8px);
}
.mask {
  opacity: 0.99;
}

They are visual effects, but they also affect stacking behavior. Animation, drag-and-drop, modal, and tooltip bugs often combine stacking context issues with rendering performance. See Reflow and Repaint: Why CSS Animations Get Janky for the performance side.

When isolation: isolate helps

You can intentionally create a separate stacking context:

.surface {
  isolation: isolate;
}

This is useful when a component should manage its own internal layers without leaking into the surrounding page.

It is not a universal fix. It creates a clearer boundary, but you still need a page-level layer strategy for modals, drawers, headers, and popovers.

Debugging order

  1. Confirm whether the element is positioned or otherwise eligible for z-index.
  2. Inspect parent elements for stacking context triggers.
  3. Compare sibling parent contexts before comparing child numbers.
  4. Decide whether the fix is a parent z-index, DOM movement, portal, fixed positioning, or top-layer API.

If you keep increasing a number from 20 to 9999 and nothing changes, the issue is probably the comparison boundary, not the number.

When not to keep raising z-index

Avoid random z-index escalation when:

  • A global modal is behind a local tooltip.
  • Sticky headers, drawers, and popovers all define their own local scales.
  • A component library and app code maintain unrelated layer constants.
  • Parent containers have accidental transform or opacity.

Use a small global layer scale for app-wide overlays, and use portals when a local DOM position creates the wrong boundary.

Subscribe to FreeMac

Weekly picks: free Mac software reviews, trusted source updates, alternatives, and low-friction guides.