z-index & Stacking Contexts
When elements overlap, something has to decide which appears on top. z-index controls that order for positioned elements — but it only works within a stacking context, and misunderstanding stacking contexts is why "z-index: 9999 doesn't work" is such a common complaint.
Default stacking order
Without z-index, later elements in the HTML paint on top of earlier ones. Positioned elements paint above non-positioned ones.
Changing the order with z-index
Higher z-index values appear in front. It applies to positioned elements (relative, absolute, fixed, sticky) and to flex and grid items. Negative values are allowed and can push an element behind its parent's content.
Stacking contexts
A stacking context is a group whose children are stacked among themselves, and then the whole group is placed as one unit in its parent. A child can never escape its group, no matter how high its z-index:
The pink child is trapped inside its parent's stacking context, which sits at level 1. The green sibling at level 2 beats the whole group.
What creates a stacking context
- The root element (
<html>). - A positioned element with a
z-indexother thanauto. - A flex or grid item with a
z-index. opacityless than 1.transform,filter,perspective,clip-path,maskorbackdrop-filterother thannone.position: fixedorsticky.isolation: isolate,mix-blend-mode,will-changewith some values,contain: paint.
The surprising ones — like opacity and transform — are usually behind z-index mysteries.
isolation: isolate
isolation: isolate creates a stacking context without any other side effects. It's perfect for components that use negative z-index decorations: without it, z-index: -1 would slip behind the page background.
Managing z-index in a project
Random values like z-index: 999 and 99999 escalate quickly. Define a small scale once and reuse it:
:root {
--z-dropdown: 10;
--z-sticky: 20;
--z-overlay: 30;
--z-modal: 40;
--z-toast: 50;
}
.site-header { position: sticky; top: 0; z-index: var(--z-sticky); }
.modal { position: fixed; z-index: var(--z-modal); }
Also remember that dialogs and popovers opened through HTML's top layer (lesson 38 of the HTML course) always appear above everything, with no z-index needed.
Debugging overlap
- Is the element positioned (or a flex/grid item)? Otherwise
z-indexdoes nothing. - Which stacking context is it in? Look up its ancestors for
z-index,opacity,transformorfilter. - Compare the stacking contexts' levels, not the children's values.
Chromium dev tools include a "Layers" panel, and extensions can visualise stacking contexts.