How z-index Works
Z-index controls the stacking order of elements on the Z-axis (depth — toward/away from the viewer). Higher values appear on top of lower values.
An element must have position set to relative, absolute, fixed, or sticky for z-index to have any effect. On position: static elements, z-index is completely ignored.
/* z-index works on positioned elements only */
.box-a {
position: absolute;
z-index: 1; /* below box-b */
}
.box-b {
position: absolute;
z-index: 2; /* above box-a */
}
/* Negative z-index — behind normal flow elements */
.watermark {
position: absolute;
z-index: -1;
}
/* z-index: auto — removed from stacking context participation */
.item { position: relative; z-index: auto; }
/* Without z-index on positioned elements:
later elements in source order appear on top */
Default Stacking Order (No z-index)
Without z-index, the browser stacks elements in this order (back to front):
- Background and borders of the root element
- Block-level elements in normal flow (back to front in source order)
- Floated elements
- Inline elements in normal flow
- Positioned elements (without z-index or z-index: auto) — in source order
The later an element appears in the HTML source, the higher it stacks (all else equal).
/* Without z-index, the second positioned element appears on top */
.red-box { position: absolute; top: 10px; left: 10px; }
.blue-box { position: absolute; top: 30px; left: 30px; }
/* blue-box appears on top because it's later in the source */
Stacking Contexts – The Key Concept
A stacking context is an isolated layer in the page's z-axis hierarchy. Elements inside a stacking context are stacked relative to each other, not relative to the entire page. The stacking context as a whole is then treated as a single flat element by its parent context.
This is why z-index: 9999 sometimes fails — if the element is inside a stacking context, its z-index only matters within that context, not against elements outside it.
/* Parent creates a stacking context (opacity < 1) */
.parent {
opacity: 0.99; /* creates a stacking context! */
position: relative;
z-index: 1;
}
/* Child has huge z-index but is trapped inside parent's context */
.modal-inside-parent {
position: absolute;
z-index: 9999; /* only competes within .parent, not the global page */
}
/* An element outside .parent with z-index: 2 will appear ABOVE
the modal, even though modal has z-index: 9999 */
.outside-element {
position: relative;
z-index: 2; /* beats parent's z-index: 1, so it's on top */
}
What Creates a Stacking Context
A new stacking context is created when an element has:
/* The most common triggers: */
/* 1. Position + z-index (not auto) */
position: relative; z-index: 1;
position: absolute; z-index: 0;
position: fixed; /* always creates a context (z-index not needed) */
position: sticky; /* always creates a context */
/* 2. Opacity less than 1 */
opacity: 0.99; /* even 0.99 creates a context! */
/* 3. Transform (not none) */
transform: translateX(0); /* even a no-op transform creates context */
transform: rotate(0deg);
/* 4. Filter (not none) */
filter: blur(0px);
/* 5. will-change targeting an above property */
will-change: transform;
will-change: opacity;
/* 6. isolation: isolate — explicitly create a context */
isolation: isolate; /* clean way to create a context intentionally */
/* 7. Various others */
mix-blend-mode: /* any value other than normal */;
clip-path: /* any value other than none */;
-webkit-overflow-scrolling: touch;
isolation: isolate creates a stacking context without any visual side effects (unlike opacity or transform). Use it when you want a component's internal z-index values to be isolated from the rest of the page — preventing z-index conflicts in large applications.
Debugging z-index Issues
/* Step 1: Is position set? z-index only works on positioned elements */
.problem-element { position: relative; } /* or absolute/fixed/sticky */
/* Step 2: Is an ancestor creating a stacking context? */
/* Check ancestors for: opacity < 1, transform, filter, will-change */
/* Open DevTools → look for "Stacking Context" badge in Layers panel */
/* Step 3: Is isolation: isolate needed? */
.component { isolation: isolate; }
/* Step 4: Use a z-index scale and stick to it */
:root {
--z-behind: -1;
--z-base: 0;
--z-raised: 1;
--z-dropdown: 10;
--z-sticky: 20;
--z-fixed: 30;
--z-overlay: 40;
--z-modal: 50;
--z-toast: 60;
--z-tooltip: 70;
}
Building a Z-index Scale
Ad-hoc z-index values (z-index: 9999, z-index: 99999) lead to maintenance nightmares. Define a design system scale at the start of every project:
/* Define at :root */
:root {
--z-behind: -1; /* watermarks, decorative backgrounds */
--z-base: 0; /* default positioned elements */
--z-raised: 1; /* slightly elevated cards */
--z-dropdown: 100; /* dropdowns, select menus */
--z-sticky: 200; /* sticky headers, sticky sidebar */
--z-fixed: 300; /* fixed header, fixed fab buttons */
--z-overlay: 400; /* modal backdrop/overlay */
--z-modal: 500; /* modal dialog */
--z-toast: 600; /* toast notifications */
--z-tooltip: 700; /* tooltips (always on top) */
}
/* Use in components */
.site-header { position: sticky; top: 0; z-index: var(--z-sticky); }
.dropdown-menu { position: absolute; z-index: var(--z-dropdown); }
.modal-overlay { position: fixed; inset: 0; z-index: var(--z-overlay); }
.modal { position: fixed; z-index: var(--z-modal); }
.toast { position: fixed; z-index: var(--z-toast); }
📋 Summary
- z-index only works on positioned elements (
position≠static). - Higher z-index = appears on top. Negative z-index = behind normal flow.
- Without z-index, later elements in source order stack on top.
- Stacking context — an isolated z-axis layer. Created by: position+z-index, opacity<1, transform, filter, will-change, isolation:isolate.
- Elements inside a stacking context compete with each other, not the global page.
- isolation: isolate — explicitly creates a stacking context with no visual side effects.
- Use CSS custom properties to define a z-index scale. Never use random large numbers.
- Debug in Chrome DevTools → Layers panel to see stacking contexts.
Frequently Asked Questions
The most common cause: a stacking context. If any ancestor of your element has opacity < 1, transform, filter, will-change, or is a positioned element with a z-index value, that ancestor creates a stacking context. Your element's z-index only competes within that context — not against the rest of the page. The fix: move the element higher in the DOM (out of the problematic ancestor), or check and remove the context-creating property from the ancestor.
isolation: isolate creates a stacking context intentionally and cleanly — no side effects like opacity or transform create. It's used on components where you want the internal z-index values to be scoped: a card component might have isolation: isolate so its badge overlay (z-index: 1) doesn't conflict with the page's modal (z-index: 500). It's the "clean" way to create isolated z-index namespaces.
In Chrome DevTools: open the Layers panel (three-dot menu → More tools → Layers). This shows all stacking contexts as separate layers with their reasons. Alternatively, in the Elements panel, stacking contexts are sometimes indicated. The manual approach: walk up the DOM tree and check each ancestor for opacity < 1, transform, filter, will-change, position + z-index, isolation: isolate, and mix-blend-mode.
Decimals are technically invalid — z-index must be an integer. Negative integers are valid and place the element behind its stacking context's background. z-index: -1 on a positioned element places it behind unpositioned siblings (behind normal flow) but still above the parent's background. This is useful for decorative background elements that should appear behind the content but inside the parent.