Stacking context and z-index

Why z-index sometimes does nothing, what creates a stacking context, and a method for debugging layering.

  • Beginner
  • 7 min read
  • Updated 17 Oct 2026

What you will learn

  • Explain how browsers order overlapping elements
  • Explain what creates a stacking context
  • Fix z-index bugs
  • Organise z-index values in a project

You set z-index: 9999 on a dropdown and it still appears behind the next section. This is one of the most common CSS frustrations, and it is almost never fixed by a bigger number. It is fixed by understanding stacking contexts.

How overlapping elements are ordered

When elements overlap, the browser paints them in a set order. Within one stacking context, from back to front:

  1. The background and borders of the element that forms the context.
  2. Positioned children with a negative z-index.
  3. Normal, non-positioned block content, in source order.
  4. Floats.
  5. Inline content, such as text.
  6. Positioned children with z-index: auto or 0, in source order.
  7. Positioned children with a positive z-index, lowest first.

Two practical consequences follow. Later elements in the HTML paint over earlier ones when nothing else differs. And a positioned element paints over non-positioned siblings, even with no z-index at all.

What z-index does

z-index sets an element's level within its stacking context. Higher numbers are closer to the viewer.

CSS
.dropdown {
    position: absolute;
    z-index: 10;
}

z-index only works on elements that are positioned (any position except static), or that are flex or grid items. On a static block, it does nothing.

What a stacking context is

A stacking context is a group of elements that is layered as one unit. Its children are ordered among themselves, then the whole group is placed into its parent context as a single layer.

The rule that explains every z-index mystery is:

A child can never escape its stacking context. Its z-index only competes with elements inside the same context.

Think of it like chapters and sections. Section 2.99 still comes before section 3.1, however big its number. A z-index: 9999 inside a context with z-index: 1 is still below a sibling context with z-index: 2.

An example of the bug

HTML
<header class="header">
    <nav>
        <ul class="dropdown">...</ul>
    </nav>
</header>
<main class="hero">...</main>
CSS
.header {
    position: relative;
    z-index: 1;               /* creates a stacking context */
}

.dropdown {
    position: absolute;
    z-index: 9999;            /* only competes inside .header */
}

.hero {
    position: relative;
    z-index: 2;               /* the whole hero is above the whole header */
}

The dropdown appears behind .hero. Its 9999 is compared only with things inside .header. At the top level, .header (1) loses to .hero (2).

The fix is to change the order of the contexts, not the dropdown:

CSS
.header {
    position: relative;
    z-index: 2;
}

.hero {
    position: relative;
    z-index: 1;
}

Or remove the z-index from .hero if it did not need one.

What creates a stacking context

The root <html> element always forms one. So does any element with:

  • position: relative or absolute and a z-index other than auto.
  • position: fixed or sticky (always, with or without z-index).
  • A flex or grid item with a z-index other than auto.
  • opacity less than 1.
  • transform, filter, backdrop-filter, perspective, clip-path or mask set to anything other than none.
  • mix-blend-mode other than normal.
  • isolation: isolate.
  • will-change naming one of the properties above.
  • contain: layout, contain: paint, or a value that includes them, such as contain: strict.
  • container-type: size or inline-size, because it applies containment.

Many of these are not "layering" properties at all. A fade-in animation that sets opacity: 0.99, or a hover effect with transform, can silently create a new context and trap a dropdown inside it.

When a z-index bug appears "suddenly", look for a recently added transform, opacity, filter or will-change on an ancestor. These are the usual culprits.

isolation: isolate

Sometimes you want a new stacking context, for example to keep a component's internal z-index values from interacting with the rest of the page. isolation: isolate creates one with no other side effects:

CSS
.card {
    isolation: isolate;
}

.card-image {
    position: relative;
    z-index: -1;   /* behind the card's text, but not behind the page */
}

Without isolation: isolate on the card, the image with z-index: -1 could drop behind the card's own background, or even behind the page.

A method for debugging layering

  1. Identify the two elements that overlap in the wrong order.
  2. Find each one's stacking context. Walk up the ancestors in the Elements panel and look for any property from the list above.
  3. Find the nearest ancestors that are siblings in the same context. Those are the two layers actually being compared.
  4. Compare their z-index and source order. Fix the layering at that level.
  5. Remove unnecessary contexts. Delete a z-index that was not needed, or move a transform to a child element.

Chrome and Edge include a Layers panel (open the command menu with Ctrl+Shift+P, or Cmd+Shift+P on macOS, and type "Layers") that shows how the page is split into compositing layers. It is useful, though compositing layers and stacking contexts are not exactly the same thing. Hovering over an element in the Elements panel and checking the Computed tab for z-index, position, transform and opacity is usually faster.

Elements rendered in the browser's top layer, such as a <dialog> opened with showModal() and elements using the popover attribute, sit above everything else regardless of z-index. For modals and menus, these native features avoid z-index battles entirely.

Organising z-index values

Random values like 10, 999 and 99999 scattered through a stylesheet are hard to reason about. Use a small, named scale with custom properties:

CSS
:root {
    --z-dropdown: 100;
    --z-sticky: 200;
    --z-overlay: 300;
    --z-toast: 400;
}

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

.dropdown-menu {
    position: absolute;
    z-index: var(--z-dropdown);
}

Keep components flat inside, with values like 1 and 2, and use isolation: isolate on the component so they never leak out. Custom properties are covered properly in a later lesson.

Summary

  • Overlapping elements are painted in a set order; later and positioned elements paint on top.
  • z-index works on positioned elements and on flex and grid items.
  • A stacking context groups its children; a child can never rise above its context's siblings.
  • opacity, transform, filter, will-change, isolation, fixed and sticky positioning all create contexts.
  • Debug by finding the two competing contexts; fix layering there, not with bigger numbers.