The 16 Ways CSS Creates Stacking Contexts
z-index: 9999 still behind? Stacking contexts are the reason. Opacity, transform, filter, and 12+ other properties silently create them—trapping your elements.
You set z-index: 9999 and the element still renders behind something with
z-index: 1. The culprit? Stacking contexts. Each stacking context is an
independent layer—z-index only compares elements within the same context.
Here are all the ways CSS silently creates them.
The Mental Model
Think of stacking contexts as transparent folders. Elements inside a folder can be reordered
relative to each other (via z-index), but the entire folder is positioned as
a unit relative to other folders. A z-index: 9999 element inside folder A
can't escape above folder B if folder B is stacked higher than folder A.
<!-- Folder A: z-index: 1 on parent -->
<div style="position: relative; z-index: 1">
<div style="position: relative; z-index: 9999">
I'm trapped in this stacking context!
</div>
</div>
<!-- Folder B: z-index: 2 on parent -->
<div style="position: relative; z-index: 2">
<div style="position: relative; z-index: 1">
I win! My parent context is higher.
</div>
</div>All Stacking Context Triggers
Per the CSS spec, these properties and conditions create a new stacking context:
1. The Root Element
<html> <!-- Always a stacking context -->The document root is always a stacking context. All other contexts are nested within it.
2. Positioned Elements with z-index
/* Classic trigger */
.modal {
position: absolute; /* or relative, fixed, sticky */
z-index: 10; /* NOT auto */
}
position: absolute/relative + z-index (not auto)
creates a stacking context. This is the most well-known trigger.
3. Fixed and Sticky Positioning
/* Always creates stacking context (even without z-index!) */
.sticky-header {
position: fixed; /* or sticky */
/* z-index not required */
}
Unlike absolute/relative, fixed and sticky elements always
create a stacking context, even without explicit z-index.
4. Flexbox/Grid Children with z-index
.flex-container {
display: flex;
}
.flex-item {
z-index: 1; /* Creates stacking context WITHOUT position! */
}
Flex and grid items can use z-index directly. Setting any value (including
z-index: 0) creates a stacking context—no positioning required.
5. Opacity Less Than 1
/* Surprise! This creates a stacking context */
.faded {
opacity: 0.99;
}
Any opacity value below 1 creates a stacking context. This is why
opacity transitions can unexpectedly change stacking behavior.
6. Transform (Any Value Except none)
/* All of these create stacking contexts */
.transformed {
transform: translateX(0); /* Even "no-op" transforms */
transform: scale(1);
transform: rotate(0deg);
}
Even transforms that visually do nothing (translateX(0)) create a stacking
context. The scale, rotate, and translate properties
(shorthand alternatives to transform) behave the same way.
7. Filter and Backdrop-Filter
.blurred {
filter: blur(5px); /* Creates stacking context */
backdrop-filter: blur(10px); /* Also creates stacking context */
}8. Perspective
.perspective-container {
perspective: 1000px; /* Creates stacking context */
}
.perspective-child {
transform: perspective(500px); /* Also creates context (via transform) */
}9. Clip-Path
.clipped {
clip-path: circle(50%); /* Creates stacking context */
clip-path: inset(10px 20px); /* Same effect */
}10. Mask Properties
.masked {
mask: url(mask.svg); /* Creates stacking context */
mask-image: linear-gradient(black, transparent);
mask-border: url(border.png) 30;
}11. Mix-Blend-Mode
.blended {
mix-blend-mode: multiply; /* Creates stacking context */
}
Any value except normal creates a stacking context.
12. Isolation
.isolated {
isolation: isolate; /* Explicit stacking context creation */
}
isolation: isolate exists specifically to create a stacking context
without side effects. Use it when you need to control stacking but don't want
visual changes from opacity/transform.
13. Will-Change (Specific Values)
.will-change {
will-change: opacity; /* Creates stacking context */
will-change: transform; /* Creates stacking context */
}
will-change hints to the browser that a property will animate. For properties
that create stacking contexts (like opacity, transform), the hint alone triggers one.
14. Contain: Layout or Paint
.contained {
contain: layout; /* Creates stacking context */
contain: paint; /* Creates stacking context */
contain: strict; /* Includes layout + paint */
contain: content; /* Includes layout + paint */
}15. Container Queries
.query-container {
container-type: size; /* Creates stacking context */
container-type: inline-size; /* Creates stacking context */
}16. Top Layer Elements
<dialog open>...</dialog> <!-- Top layer = stacking context -->
<div popover>...</div> <!-- When open -->
Elements promoted to the top layer (fullscreen, dialogs, popovers) and their
::backdrop pseudo-elements are stacking contexts.
The Gotcha: Animations
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.animated {
animation: fadeIn 1s forwards;
}
/* During animation: stacking context (opacity < 1)
After animation: stacking context persists if forwards fill mode */
Animating stacking-context-creating properties temporarily creates a context during
the animation. With animation-fill-mode: forwards, it persists after.
Debugging Stacking Issues
Chrome DevTools: Layers Panel
- Open DevTools (F12)
- Press
Cmd/Ctrl + Shift + P→ "Show Layers" - Visualize stacking contexts as 3D layers
Manual Inspection
// Find all stacking contexts on a page (rough detection)
const triggers = [
'[style*="position: fixed"]',
'[style*="position: sticky"]',
'[style*="opacity"]',
'[style*="transform"]',
'[style*="filter"]',
// ... more checks needed for computed styles
];
// Better: check computed styles
function createsStackingContext(element) {
const style = getComputedStyle(element);
if (style.position === 'fixed' || style.position === 'sticky') return true;
if (style.zIndex !== 'auto' && style.position !== 'static') return true;
if (parseFloat(style.opacity) < 1) return true;
if (style.transform !== 'none') return true;
if (style.filter !== 'none') return true;
if (style.isolation === 'isolate') return true;
// ... more checks
return false;
}Fixing Common Problems
Modal Behind Overlay
/* Problem: Modal trapped in parent's stacking context */
.parent {
position: relative;
z-index: 1;
}
.modal {
position: fixed;
z-index: 9999; /* Won't escape parent! */
}
/* Solution: Move modal outside parent, or remove parent's context */
.parent {
position: relative;
/* Remove z-index, or set to auto */
}Tooltip Behind Sibling
/* Problem: Card with transform blocks tooltip */
.card {
transform: translateY(0); /* Creates stacking context! */
}
.tooltip {
position: absolute;
z-index: 100; /* Trapped inside .card's context */
}
/* Solution: Use isolation on tooltip's parent */
.tooltip-wrapper {
isolation: isolate;
}Best Practices
- Use
isolation: isolatewhen you intentionally want a stacking context without visual side effects - Minimize z-index values—if you need 9999, something is wrong with your stacking architecture
- Document stacking contexts in your component library; note which components create them
- Check animations—opacity and transform animations temporarily create contexts
- Use CSS custom properties for z-index—
--z-modal: 100; --z-tooltip: 200;
Quick Reference Table
| Property/Condition | Creates Context? |
|---------------------------------|------------------|
| position: fixed/sticky | Always |
| position: abs/rel + z-index | Yes (not auto) |
| flex/grid item + z-index | Yes (any value) |
| opacity < 1 | Yes |
| transform ≠ none | Yes |
| filter ≠ none | Yes |
| backdrop-filter ≠ none | Yes |
| perspective ≠ none | Yes |
| clip-path ≠ none | Yes |
| mask/mask-image/mask-border | Yes |
| mix-blend-mode ≠ normal | Yes |
| isolation: isolate | Yes |
| will-change: opacity/transform | Yes |
| contain: layout/paint | Yes |
| container-type: size/inline | Yes |
| Top layer (dialog, popover) | Yes |Key Takeaways
z-indexonly works within a stacking context—not globally- 16+ CSS properties silently create stacking contexts
opacity: 0.99andtransform: translateX(0)create contexts- Use
isolation: isolateto explicitly create contexts without side effects - Debug with Chrome's Layers panel to visualize context hierarchy
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Related Insights
Explore related edge cases and patterns
Advertisement