Episode 1 — Fundamentals / 1.8 — CSS Layout Mastery
1.8.f — Stacking Context
In one sentence: A stacking context is an invisible boundary that determines how elements overlap —
z-indexonly competes within the same stacking context, which is why "just addz-index: 9999" often does not work.
Navigation: ← 1.8.e — Positioning · 1.8.g — Container Queries →
1. What creates a stacking context
A new stacking context is formed by any element with:
| Trigger | Example |
|---|---|
position: relative/absolute + z-index (not auto) | position: relative; z-index: 1; |
position: fixed or sticky | Always creates one |
opacity less than 1 | opacity: 0.99; — yes, even 0.99 |
transform (any value except none) | transform: translateZ(0); |
filter (any value except none) | filter: blur(0); |
isolation: isolate | Explicit context creation |
Flex/grid child with z-index (not auto) | z-index: 1 on a flex item |
will-change with certain values | will-change: transform; |
contain: paint or contain: layout | CSS containment |
mix-blend-mode (not normal) | mix-blend-mode: multiply; |
Key insight: The root element (<html>) is always a stacking context. Every other context is nested inside it.
2. z-index only works within context
z-index does not create a global layer system. It only determines stacking among siblings within the same stacking context.
Root stacking context (html)
├── Header (z-index: 100) ← context A
│ ├── Logo (z-index: 5) ← competes within A only
│ └── Dropdown (z-index: 999) ← competes within A only
│
└── Modal (z-index: 50) ← context B
└── Close button (z-index: 1) ← competes within B only
Even though the dropdown has z-index: 999, it cannot appear above the modal if the header's context (z-index: 100) is higher than the modal's context (z-index: 50). Wait — in this case the header IS higher (100 > 50), so the dropdown WOULD appear above. The bug happens in the reverse: if the header had z-index: 10 and the modal z-index: 50, no z-index on the dropdown could lift it above the modal.
3. Default paint order (no z-index)
Within a single stacking context, elements paint in this order (back to front):
- Background and borders of the stacking context root
- Negative z-index children (e.g.
z-index: -1) - Block-level elements in DOM order (normal flow)
- Floated elements
- Inline/inline-block elements in DOM order
- z-index: 0 and z-index: auto positioned elements
- Positive z-index children, in ascending order
Practical implication: A positioned element with z-index: auto still paints above non-positioned block elements in the same context.
4. Common z-index bugs
Bug 1: "z-index has no effect"
/* z-index is ignored on static elements */
.box {
z-index: 100; /* does nothing — position is static */
}
Fix: Add position: relative (or the element must be a flex/grid child).
Bug 2: Modal appears behind sidebar
.sidebar {
position: relative;
z-index: 10; /* creates a stacking context */
}
.modal {
position: fixed;
z-index: 9999; /* but modal is INSIDE sidebar in the DOM */
}
If the modal is a DOM descendant of .sidebar, its z-index: 9999 only competes within the sidebar's stacking context. It cannot escape above siblings of .sidebar that have higher z-index.
Fix: Move the modal to a portal at the top level of the DOM (outside sidebar).
Bug 3: Unintentional stacking context from transform or opacity
.parent {
transform: translateZ(0); /* GPU hint — creates stacking context */
}
.child {
position: fixed; /* now fixed relative to .parent, not viewport! */
}
Fix: Be aware that transform, filter, opacity < 1, and will-change all create stacking contexts and alter containing blocks.
Bug 4: z-index arms race
.dropdown { z-index: 100; }
.toast { z-index: 999; }
.modal { z-index: 9999; }
.tooltip { z-index: 99999; } /* where does it end? */
This is unmaintainable. Use a token-based system instead.
5. Token-based z-index system
Define z-index values as design tokens with clear semantic layers:
:root {
--z-base: 0;
--z-dropdown: 100;
--z-sticky: 200;
--z-overlay: 300;
--z-modal: 400;
--z-toast: 500;
--z-tooltip: 600;
}
.dropdown { z-index: var(--z-dropdown); }
.sticky-header { z-index: var(--z-sticky); }
.modal-backdrop { z-index: var(--z-modal); }
.toast { z-index: var(--z-toast); }
.tooltip { z-index: var(--z-tooltip); }
Benefits:
- Every layer has a clear name and purpose.
- Gaps between values (100, 200, …) leave room for additions.
- No magic numbers scattered through the codebase.
- Easy to audit by searching for
--z-.
6. isolation: isolate
Sometimes you want to create a stacking context without side effects (no positioning, no opacity change). That is what isolation: isolate does:
.card {
isolation: isolate; /* new stacking context, nothing else changes */
}
This is useful for component libraries — it ensures a component's internal z-index values cannot leak out and conflict with the rest of the page.
7. Debugging stacking with DevTools
Chrome DevTools
- Elements panel → Computed → z-index: See the computed z-index value.
- 3D View (Layers panel):
Ctrl+Shift+P→ "Show Layers" → visualize stacking contexts as 3D layers. - Elements panel → highlight: Hover over elements to see their bounds and stacking.
Debugging workflow
- Identify which element is not stacking correctly.
- Walk up the DOM — find which ancestor creates a stacking context.
- Check if the z-index values compete in the same context or different ones.
- If different contexts, the fix is usually: restructure DOM, adjust ancestor z-index, or use a portal.
8. Key takeaways
- z-index is not global — it only competes within the same stacking context.
- Many properties create stacking contexts:
opacity,transform,filter,position+z-index,isolation. - Paint order matters even without z-index — positioned elements paint above non-positioned ones.
- Use a token-based z-index system to avoid arms races.
- Use
isolation: isolateto contain z-index within components. - Debug with the Layers panel and by walking the DOM to find context boundaries.
Explain-It Challenge
Explain without notes:
- Why can a
z-index: 9999element still appear behind az-index: 1element? - Name three CSS properties (besides
position+z-index) that create a stacking context. - How does
isolation: isolatehelp in a component-based architecture?
Navigation: ← 1.8.e — Positioning · 1.8.g — Container Queries →