z-index and Stacking Contexts
z-index controls layering — but it only works within a stacking context, which explains why z-index 9999 sometimes has no effect at all.
- Explain what z-index controls and which elements it applies to
- Define what a stacking context is and why it creates an isolated layering group
- List the CSS properties that create a new stacking context
- Diagnose the "z-index ignored" bug by inspecting ancestor stacking contexts
Every developer has written z-index: 9999 and watched it do nothing. The element
stays buried. You add more nines. Still nothing. The number is not the problem.
Understanding stacking contexts is.
What z-index does (and does not do)
z-index sets the stack level of an element within its stacking context. Higher
values draw on top. It only applies to:
- Positioned elements — anything with
positionother thanstatic. - Flex items and grid items — even without
position: relative.
On a plain position: static element, z-index is completely ignored, regardless
of the value. This is the first thing to check when z-index appears to have no
effect.
/* z-index ignored: static element */
.box { z-index: 10; }
/* z-index works: positioned element */
.box { position: relative; z-index: 10; }What a stacking context is
A stacking context is a self-contained layering group. Elements inside it are
painted together as a unit, and their z-index values only compete with siblings
inside the same stacking context — never with elements in a different context.
Think of it as a separate layer in a design tool. You can arrange items within a
layer freely, but the entire layer sits above or below other layers as a unit. An
item deep inside a low-priority layer cannot reach up through it to appear above an
item in a high-priority layer, no matter how large its z-index is.
Stacking context: body (root)
├── .modal-backdrop (z-index: 50) ← paints on top
│ └── .modal (z-index: 1) ← 1 within its context, still on top of everything below
└── .header (z-index: 100) ← 100 within root context
└── .dropdown (z-index: 9999) ← 9999 within header's context onlyIn this tree, .dropdown loses to .modal-backdrop even though 9999 is greater
than 50. The comparison never happens: .dropdown is inside .header's stacking
context, and .header's context (z-index 100) loses to .modal-backdrop (z-index
50) — wait, 100 is greater than 50, so .header wins… but .modal-backdrop is
trying to sit above the header. The example illustrates that the comparison happens
at the stacking context level first, before any child z-indexes are evaluated.
The root stacking context is established by the <html> element. Every z-index
comparison ultimately resolves in the context of a common ancestor. Finding that
ancestor is the key to debugging z-index problems.
What creates a new stacking context
The list is longer than most developers realize:
| Property | Condition |
|---|---|
position | with z-index other than auto |
opacity | less than 1 |
transform | any value other than none |
filter | any value other than none |
will-change | any value that would trigger a stacking context |
isolation | isolate |
mix-blend-mode | any value other than normal |
The surprising ones are opacity, transform, and filter. Adding
transform: translateZ(0) to an element as a "performance hack" silently creates
a new stacking context, which can trap child elements that were previously escaping
to a higher context.
The debugging approach
When z-index is not working:
-
Check that the element has
positionset (anything butstatic) or is a flex/grid item. Noposition, noz-index. -
Find every ancestor from the element up to the root. For each ancestor, check whether it has any of the stacking-context-creating properties listed above.
-
If an ancestor has a stacking context, your element's
z-indexonly applies within that ancestor's context. The ancestor's ownz-index(relative to its context) determines how the whole group stacks against the rest of the page. -
The fix is usually one of:
- Move the element higher in the DOM tree, outside the limiting ancestor.
- Raise the ancestor's own
z-indexso its context sits above what is covering it. - Remove the property on the ancestor that is creating the unintended stacking
context (commonly an unnecessary
transform: translateZ(0)).
/* Before: modal inside a transformed container — z-index is trapped */
.page-section {
transform: translateX(0); /* creates a stacking context */
}
.modal {
position: fixed;
z-index: 1000; /* only matters within .page-section's context */
}
/* Fix: remove the transform or restructure so the modal is outside the section */
.page-section {
/* transform removed; use a different approach to the original problem */
}z-index: 9999 is almost always a symptom, not a solution. When you reach for
a large number, it means something in the stacking context tree is not structured
the way you think. Investigate the ancestor chain instead of escalating the number.
The isolation shortcut
If you want to contain a component's z-index values without introducing an
unwanted transform or opacity, isolation: isolate creates a stacking context
with no other visual side effects:
.modal-container {
isolation: isolate; /* creates a stacking context cleanly */
}This is the intended way to scope z-index to a component boundary in design systems. It says: "everything inside this element competes with each other for layering; nothing inside escapes to compete with elements outside."
Where to go next
You have now covered every major CSS layout system: normal flow, Flexbox, Grid, positioning, and stacking. The final stop in this module is hands-on practice: Lab — Layout Challenge — build a two-column blog layout and a responsive card grid from scratch, applying everything from this module.
Positioning
CSS positioning removes elements from normal flow to place them at explicit coordinates — four values with distinct behavior that learners frequently confuse.
Lab — Layout Challenge
Build a two-column blog page layout and a card grid using Grid for the page structure and Flexbox for the component interiors — no floats, no absolute positioning tricks.