Ad – 728×90
📐 CSS Layout

CSS Z-index – Stacking Order and Stacking Contexts

Why does your modal appear behind the dropdown? Why does setting z-index: 9999 sometimes not work? The answer lies in one of CSS's least-understood mechanisms: stacking contexts. Z-index controls the stacking order of overlapping elements, but it only works within the rules of its stacking context. In this lesson you will learn exactly how z-index works, what creates a stacking context, and how to build a maintainable z-index system for your project.

⏱️ 18 min read 🎯 Beginner–Intermediate 📅 Updated 2026 👁️ Lesson 6 of 6

How z-index Works

Z-index controls the stacking order of elements on the Z-axis (depth — toward/away from the viewer). Higher values appear on top of lower values.

⚠️
z-index only works on positioned elements

An element must have position set to relative, absolute, fixed, or sticky for z-index to have any effect. On position: static elements, z-index is completely ignored.

CSS – z-index basics
/* z-index works on positioned elements only */
.box-a {
  position: absolute;
  z-index: 1;      /* below box-b */
}

.box-b {
  position: absolute;
  z-index: 2;      /* above box-a */
}

/* Negative z-index — behind normal flow elements */
.watermark {
  position: absolute;
  z-index: -1;
}

/* z-index: auto — removed from stacking context participation */
.item { position: relative; z-index: auto; }

/* Without z-index on positioned elements:
   later elements in source order appear on top */
z-index: 1
z-index: 3
z-index: 2

Default Stacking Order (No z-index)

Without z-index, the browser stacks elements in this order (back to front):

  1. Background and borders of the root element
  2. Block-level elements in normal flow (back to front in source order)
  3. Floated elements
  4. Inline elements in normal flow
  5. Positioned elements (without z-index or z-index: auto) — in source order

The later an element appears in the HTML source, the higher it stacks (all else equal).

CSS – Source order stacking
/* Without z-index, the second positioned element appears on top */
.red-box  { position: absolute; top: 10px; left: 10px; }
.blue-box { position: absolute; top: 30px; left: 30px; }
/* blue-box appears on top because it's later in the source */
Ad – 336×280

Stacking Contexts – The Key Concept

A stacking context is an isolated layer in the page's z-axis hierarchy. Elements inside a stacking context are stacked relative to each other, not relative to the entire page. The stacking context as a whole is then treated as a single flat element by its parent context.

This is why z-index: 9999 sometimes fails — if the element is inside a stacking context, its z-index only matters within that context, not against elements outside it.

CSS – Stacking context failure example
/* Parent creates a stacking context (opacity < 1) */
.parent {
  opacity: 0.99;       /* creates a stacking context! */
  position: relative;
  z-index: 1;
}

/* Child has huge z-index but is trapped inside parent's context */
.modal-inside-parent {
  position: absolute;
  z-index: 9999;       /* only competes within .parent, not the global page */
}

/* An element outside .parent with z-index: 2 will appear ABOVE
   the modal, even though modal has z-index: 9999 */
.outside-element {
  position: relative;
  z-index: 2;          /* beats parent's z-index: 1, so it's on top */
}

What Creates a Stacking Context

A new stacking context is created when an element has:

CSS – Stacking context triggers
/* The most common triggers: */

/* 1. Position + z-index (not auto) */
position: relative; z-index: 1;
position: absolute; z-index: 0;
position: fixed;    /* always creates a context (z-index not needed) */
position: sticky;   /* always creates a context */

/* 2. Opacity less than 1 */
opacity: 0.99;   /* even 0.99 creates a context! */

/* 3. Transform (not none) */
transform: translateX(0);  /* even a no-op transform creates context */
transform: rotate(0deg);

/* 4. Filter (not none) */
filter: blur(0px);

/* 5. will-change targeting an above property */
will-change: transform;
will-change: opacity;

/* 6. isolation: isolate — explicitly create a context */
isolation: isolate;  /* clean way to create a context intentionally */

/* 7. Various others */
mix-blend-mode: /* any value other than normal */;
clip-path: /* any value other than none */;
-webkit-overflow-scrolling: touch;
💡
Use isolation: isolate to intentionally scope z-index

isolation: isolate creates a stacking context without any visual side effects (unlike opacity or transform). Use it when you want a component's internal z-index values to be isolated from the rest of the page — preventing z-index conflicts in large applications.

Debugging z-index Issues

CSS – Debugging checklist
/* Step 1: Is position set? z-index only works on positioned elements */
.problem-element { position: relative; } /* or absolute/fixed/sticky */

/* Step 2: Is an ancestor creating a stacking context? */
/* Check ancestors for: opacity < 1, transform, filter, will-change */
/* Open DevTools → look for "Stacking Context" badge in Layers panel */

/* Step 3: Is isolation: isolate needed? */
.component { isolation: isolate; }

/* Step 4: Use a z-index scale and stick to it */
:root {
  --z-behind:   -1;
  --z-base:      0;
  --z-raised:    1;
  --z-dropdown: 10;
  --z-sticky:   20;
  --z-fixed:    30;
  --z-overlay:  40;
  --z-modal:    50;
  --z-toast:    60;
  --z-tooltip:  70;
}

Building a Z-index Scale

Ad-hoc z-index values (z-index: 9999, z-index: 99999) lead to maintenance nightmares. Define a design system scale at the start of every project:

CSS – Production z-index system
/* Define at :root */
:root {
  --z-behind:    -1;   /* watermarks, decorative backgrounds */
  --z-base:       0;   /* default positioned elements */
  --z-raised:     1;   /* slightly elevated cards */
  --z-dropdown: 100;   /* dropdowns, select menus */
  --z-sticky:   200;   /* sticky headers, sticky sidebar */
  --z-fixed:    300;   /* fixed header, fixed fab buttons */
  --z-overlay:  400;   /* modal backdrop/overlay */
  --z-modal:    500;   /* modal dialog */
  --z-toast:    600;   /* toast notifications */
  --z-tooltip:  700;   /* tooltips (always on top) */
}

/* Use in components */
.site-header   { position: sticky; top: 0; z-index: var(--z-sticky); }
.dropdown-menu { position: absolute; z-index: var(--z-dropdown); }
.modal-overlay { position: fixed; inset: 0; z-index: var(--z-overlay); }
.modal         { position: fixed; z-index: var(--z-modal); }
.toast         { position: fixed; z-index: var(--z-toast); }

📋 Summary

  • z-index only works on positioned elements (positionstatic).
  • Higher z-index = appears on top. Negative z-index = behind normal flow.
  • Without z-index, later elements in source order stack on top.
  • Stacking context — an isolated z-axis layer. Created by: position+z-index, opacity<1, transform, filter, will-change, isolation:isolate.
  • Elements inside a stacking context compete with each other, not the global page.
  • isolation: isolate — explicitly creates a stacking context with no visual side effects.
  • Use CSS custom properties to define a z-index scale. Never use random large numbers.
  • Debug in Chrome DevTools → Layers panel to see stacking contexts.

Frequently Asked Questions

Why doesn't z-index: 9999 work? +

The most common cause: a stacking context. If any ancestor of your element has opacity < 1, transform, filter, will-change, or is a positioned element with a z-index value, that ancestor creates a stacking context. Your element's z-index only competes within that context — not against the rest of the page. The fix: move the element higher in the DOM (out of the problematic ancestor), or check and remove the context-creating property from the ancestor.

What is isolation: isolate for? +

isolation: isolate creates a stacking context intentionally and cleanly — no side effects like opacity or transform create. It's used on components where you want the internal z-index values to be scoped: a card component might have isolation: isolate so its badge overlay (z-index: 1) doesn't conflict with the page's modal (z-index: 500). It's the "clean" way to create isolated z-index namespaces.

How do I find which ancestor is creating a stacking context? +

In Chrome DevTools: open the Layers panel (three-dot menu → More tools → Layers). This shows all stacking contexts as separate layers with their reasons. Alternatively, in the Elements panel, stacking contexts are sometimes indicated. The manual approach: walk up the DOM tree and check each ancestor for opacity < 1, transform, filter, will-change, position + z-index, isolation: isolate, and mix-blend-mode.

Can z-index be a decimal or negative number? +

Decimals are technically invalid — z-index must be an integer. Negative integers are valid and place the element behind its stacking context's background. z-index: -1 on a positioned element places it behind unpositioned siblings (behind normal flow) but still above the parent's background. This is useful for decorative background elements that should appear behind the content but inside the parent.