You set z-index: 9999 on a dropdown and it still appears behind the next section. This is one of the most common CSS frustrations, and it is almost never fixed by a bigger number. It is fixed by understanding stacking contexts.
How overlapping elements are ordered
When elements overlap, the browser paints them in a set order. Within one stacking context, from back to front:
- The background and borders of the element that forms the context.
- Positioned children with a negative
z-index. - Normal, non-positioned block content, in source order.
- Floats.
- Inline content, such as text.
- Positioned children with
z-index: autoor0, in source order. - Positioned children with a positive
z-index, lowest first.
Two practical consequences follow. Later elements in the HTML paint over earlier ones when nothing else differs. And a positioned element paints over non-positioned siblings, even with no z-index at all.
What z-index does
z-index sets an element's level within its stacking context. Higher numbers are closer to the viewer.
.dropdown {
position: absolute;
z-index: 10;
}
z-index only works on elements that are positioned (any position except static), or that are flex or grid items. On a static block, it does nothing.
What a stacking context is
A stacking context is a group of elements that is layered as one unit. Its children are ordered among themselves, then the whole group is placed into its parent context as a single layer.
The rule that explains every z-index mystery is:
A child can never escape its stacking context. Its z-index only competes with elements inside the same context.
Think of it like chapters and sections. Section 2.99 still comes before section 3.1, however big its number. A z-index: 9999 inside a context with z-index: 1 is still below a sibling context with z-index: 2.
An example of the bug
<header class="header">
<nav>
<ul class="dropdown">...</ul>
</nav>
</header>
<main class="hero">...</main>
.header {
position: relative;
z-index: 1; /* creates a stacking context */
}
.dropdown {
position: absolute;
z-index: 9999; /* only competes inside .header */
}
.hero {
position: relative;
z-index: 2; /* the whole hero is above the whole header */
}
The dropdown appears behind .hero. Its 9999 is compared only with things inside .header. At the top level, .header (1) loses to .hero (2).
The fix is to change the order of the contexts, not the dropdown:
.header {
position: relative;
z-index: 2;
}
.hero {
position: relative;
z-index: 1;
}
Or remove the z-index from .hero if it did not need one.
What creates a stacking context
The root <html> element always forms one. So does any element with:
position: relativeorabsoluteand az-indexother thanauto.position: fixedorsticky(always, with or withoutz-index).- A flex or grid item with a
z-indexother thanauto. opacityless than1.transform,filter,backdrop-filter,perspective,clip-pathormaskset to anything other thannone.mix-blend-modeother thannormal.isolation: isolate.will-changenaming one of the properties above.contain: layout,contain: paint, or a value that includes them, such ascontain: strict.container-type: sizeorinline-size, because it applies containment.
Many of these are not "layering" properties at all. A fade-in animation that sets opacity: 0.99, or a hover effect with transform, can silently create a new context and trap a dropdown inside it.
When a z-index bug appears "suddenly", look for a recently added transform, opacity, filter or will-change on an ancestor. These are the usual culprits.
isolation: isolate
Sometimes you want a new stacking context, for example to keep a component's internal z-index values from interacting with the rest of the page. isolation: isolate creates one with no other side effects:
.card {
isolation: isolate;
}
.card-image {
position: relative;
z-index: -1; /* behind the card's text, but not behind the page */
}
Without isolation: isolate on the card, the image with z-index: -1 could drop behind the card's own background, or even behind the page.
A method for debugging layering
- Identify the two elements that overlap in the wrong order.
- Find each one's stacking context. Walk up the ancestors in the Elements panel and look for any property from the list above.
- Find the nearest ancestors that are siblings in the same context. Those are the two layers actually being compared.
- Compare their z-index and source order. Fix the layering at that level.
- Remove unnecessary contexts. Delete a
z-indexthat was not needed, or move atransformto a child element.
Chrome and Edge include a Layers panel (open the command menu with Ctrl+Shift+P, or Cmd+Shift+P on macOS, and type "Layers") that shows how the page is split into compositing layers. It is useful, though compositing layers and stacking contexts are not exactly the same thing. Hovering over an element in the Elements panel and checking the Computed tab for z-index, position, transform and opacity is usually faster.
Elements rendered in the browser's top layer, such as a <dialog> opened with showModal() and elements using the popover attribute, sit above everything else regardless of z-index. For modals and menus, these native features avoid z-index battles entirely.
Organising z-index values
Random values like 10, 999 and 99999 scattered through a stylesheet are hard to reason about. Use a small, named scale with custom properties:
:root {
--z-dropdown: 100;
--z-sticky: 200;
--z-overlay: 300;
--z-toast: 400;
}
.site-header {
position: sticky;
top: 0;
z-index: var(--z-sticky);
}
.dropdown-menu {
position: absolute;
z-index: var(--z-dropdown);
}
Keep components flat inside, with values like 1 and 2, and use isolation: isolate on the component so they never leak out. Custom properties are covered properly in a later lesson.
Summary
- Overlapping elements are painted in a set order; later and positioned elements paint on top.
z-indexworks on positioned elements and on flex and grid items.- A stacking context groups its children; a child can never rise above its context's siblings.
opacity,transform,filter,will-change,isolation, fixed and sticky positioning all create contexts.- Debug by finding the two competing contexts; fix layering there, not with bigger numbers.