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); }
z-index: Why Your "999999" Sometimes Does Nothing
Two rules explain nearly every z-index frustration. First: z-index only works on positioned elements (position other than static). Set z-index on a static element and it's ignored. Second: z-index is relative to a stacking context, not the whole page.
.modal {
position: fixed; /* required — z-index is ignored on static */
z-index: 100;
}
The trap: a child can't escape its parent's context
If a parent creates a stacking context (via position+z-index, opacity < 1, transform, filter, etc.), its children are stacked within it. A child with z-index: 999999 still sits below a sibling of its parent that has a higher context — no matter how huge the number:
.card { transform: scale(1); } /* ⚠️ creates a stacking context! */
.badge { z-index: 999999; } /* trapped inside .card — can't cover siblings */
Debug approach: when a high z-index "doesn't work," look up the DOM for an ancestor with transform, opacity, or filter that created a context boundary. Keep z-index values small and organized (10/20/30) instead of escalating to 9999 — the number rarely is the real problem.
🏋️ Practical Exercise
- Overlap two positioned boxes and control which is on top with
z-index. - Confirm
z-indexneeds a positioned element to work. - Create a modal overlay above the page content.
- Stack three elements in a chosen order.
- Trigger a new stacking context and observe its effect.
🔥 Challenge Exercise
Build a modal dialog: a full-screen semi-transparent overlay above the page and a centered dialog box above the overlay, with correct z-index layering. Explain why z-index only works on positioned elements and what a stacking context is.
📋 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.
Interview Questions
- What does
z-indexcontrol? - Why does
z-indexrequire apositionother thanstatic? - What is a stacking context?
- How can a parent’s stacking context trap a child’s
z-index? - What is the default stacking order without
z-index?
Related Topics
FAQ
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.

