Difficulty: Intermediate
z-index controls the stacking order of elements along the z-axis (the axis that points toward the viewer). It only works on positioned elements (anything other than position: static) and elements within a flex or grid container. A higher z-index value means the element appears in front of elements with lower values. Understanding z-index requires understanding stacking contexts, which is one of the most misunderstood concepts in CSS.
The default stacking order without z-index follows specific rules. Non-positioned elements paint in document order (later elements paint on top). Positioned elements paint on top of non-positioned elements. Among positioned elements without z-index, later elements in the source order paint on top. This means you can often control overlap through source order alone, without touching z-index.
A stacking context is a three-dimensional conceptualization of HTML elements along the z-axis. Every stacking context has a single root element, and the z-index of child elements is only meaningful within that context. The root HTML element creates the initial stacking context. New stacking contexts are created by: elements with position and a z-index value other than auto, elements with opacity less than 1, elements with transform, filter, perspective, clip-path, or mask properties, flex/grid children with z-index other than auto, and elements with isolation: isolate.
The critical concept is that stacking contexts are hierarchical and isolated. An element inside one stacking context can never appear between elements in a sibling stacking context, regardless of z-index values. If element A creates a stacking context with z-index: 1, and element B is a sibling with z-index: 2, then every child of A - even one with z-index: 9999 - will appear behind B. This is the root cause of most z-index frustrations.
The isolation property provides a clean way to create a new stacking context without side effects. Setting isolation: isolate on a container creates a stacking context that contains all z-index values within its children, preventing them from interfering with z-index values outside the container. This is the recommended approach for component-based architectures where you want to ensure a component's internal z-index values do not leak out and conflict with the rest of the page.
<style>
.stack-demo {
position: relative;
height: 200px;
margin: 20px;
}
.layer {
position: absolute;
width: 180px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
}
.red {
background: #ef4444;
top: 0;
left: 0;
z-index: 1;
}
.green {
background: #22c55e;
top: 30px;
left: 60px;
z-index: 3;
}
.blue {
background: #3b82f6;
top: 60px;
left: 120px;
z-index: 2;
}
</style>
<div class="stack-demo">
<div class="layer red">z-index: 1</div>
<div class="layer green">z-index: 3 (top)</div>
<div class="layer blue">z-index: 2</div>
</div>
Green (z-index: 3) appears in front of blue (2), which appears in front of red (1). All three are in the same stacking context (the stack-demo parent), so their z-index values are directly comparable.
<style>
.parent-a {
position: relative;
z-index: 1; /* Creates stacking context */
background: #fef3c7;
padding: 20px;
margin: 10px;
}
.parent-b {
position: relative;
z-index: 2; /* Higher than parent-a */
background: #dbeafe;
padding: 20px;
margin: 10px;
margin-top: -30px; /* Overlap with parent-a */
}
.child-high {
position: relative;
z-index: 9999; /* Very high, but trapped in parent-a's context */
background: #ef4444;
color: white;
padding: 10px;
}
.child-low {
position: relative;
z-index: 1;
background: #3b82f6;
color: white;
padding: 10px;
}
</style>
<div class="parent-a">
Parent A (z-index: 1)
<div class="child-high">Child z-index: 9999 (still behind Parent B!)</div>
</div>
<div class="parent-b">
Parent B (z-index: 2)
<div class="child-low">Child z-index: 1</div>
</div>
Even though the child in Parent A has z-index: 9999, it cannot escape Parent A's stacking context (z-index: 1). Parent B (z-index: 2) and all its children will always paint in front of Parent A and all its children.
<style>
.component {
isolation: isolate; /* Contains z-index within */
position: relative;
padding: 20px;
margin: 20px;
border: 1px solid #e2e8f0;
}
.tooltip {
position: absolute;
top: -10px;
left: 50%;
transform: translateX(-50%);
background: #1e293b;
color: white;
padding: 6px 12px;
border-radius: 4px;
z-index: 10; /* Only relevant within this component */
white-space: nowrap;
font-size: 14px;
}
.dropdown {
position: absolute;
top: 100%;
left: 0;
background: white;
border: 1px solid #e2e8f0;
padding: 8px 0;
z-index: 10; /* Same value, no conflict */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
min-width: 150px;
}
</style>
<div class="component">
<div class="tooltip">Tooltip (z-index: 10)</div>
Component A
</div>
<div class="component">
Component B
<div class="dropdown">
<div>Option 1</div>
<div>Option 2</div>
</div>
</div>
Each component uses isolation: isolate to create its own stacking context. This ensures that z-index values inside one component do not interfere with another component's z-index values. Both use z-index: 10 without conflict.
<style>
.demo { position: relative; margin: 10px; padding: 16px; }
/* These all create new stacking contexts: */
.ctx-zindex {
position: relative;
z-index: 0; /* position + z-index (not auto) */
background: #dbeafe;
}
.ctx-opacity {
opacity: 0.99; /* opacity < 1 */
background: #dcfce7;
}
.ctx-transform {
transform: translateZ(0); /* Any transform value */
background: #fef3c7;
}
.ctx-filter {
filter: blur(0px); /* Any filter value */
background: #fce7f3;
}
.ctx-isolation {
isolation: isolate; /* Explicit stacking context */
background: #f3e8ff;
}
.ctx-flex-child {
/* Flex/grid child with z-index */
z-index: 0;
background: #ecfdf5;
}
.inner {
position: absolute;
z-index: 999;
background: #ef4444;
color: white;
padding: 4px 8px;
top: -8px;
right: -8px;
}
</style>
<!-- Each of these containers creates a stacking context,
trapping the .inner element's z-index within -->
<div class="demo ctx-zindex">
z-index: 0 <span class="inner">Trapped</span>
</div>
<div class="demo ctx-opacity">
opacity: 0.99 <span class="inner">Trapped</span>
</div>
<div class="demo ctx-transform">
transform <span class="inner">Trapped</span>
</div>
Many CSS properties create stacking contexts as a side effect: z-index on positioned elements, opacity < 1, transform, filter, perspective, clip-path, mask, and isolation: isolate. This means a child's z-index: 999 is trapped within any of these parent contexts.
z-index, Stacking Context, Stacking Order, isolation, Painted Order