z-index orders overlapping elements, but only within the same stacking context. A child with z-index: 9999 cannot rise above an element outside its parent if that parent forms a context with a lower z-index. Contexts come from position plus z-index, opacity below 1, transform, filter, isolation: isolate and several other properties.
| Symptom | Likely cause |
|---|---|
| z-index has no effect | Element is position: static and not a flex or grid child |
| Huge z-index still behind | An ancestor forms a stacking context with a lower layer |
| Dropdown hidden under next section | Parent has transform, opacity or filter |
| Menu clipped at the edge | overflow: hidden on an ancestor, not z-index at all |
Updated
The 3D view below pulls a small page apart into its stacking layers so you can see which boxes are trapped inside which context. Change z-index values and toggle the property that creates a context on the parent. After it: every trigger per MDN, sticky headers with dropdowns, a token scale and a step-by-step debugging routine.
:root {
--z-base: 0; /* Normal content */
--z-raised: 10; /* Cards lifted on hover, sticky table headers */
--z-dropdown: 100; /* Menus, autocomplete lists */
--z-sticky: 200; /* Sticky site header */
--z-overlay: 300; /* Drawer backdrop */
--z-modal: 400; /* Custom modals (native dialog uses the top layer instead) */
--z-toast: 500; /* Notifications that must sit above everything */
}Based on the MDN stacking context reference. Any one condition is enough.
| Property or state | Condition |
|---|---|
| Root element | html is always the root stacking context |
| position: absolute or relative | Only with a z-index other than auto |
| position: fixed or sticky | Always, z-index not needed |
| Flex or grid child | With a z-index other than auto, even if position is static |
| container-type: size or inline-size | Any element that is a size query container |
| opacity below 1 | Including opacity: 0.99 |
| mix-blend-mode | Any value other than normal |
| transform, scale, rotate, translate | Any value other than none |
| filter or backdrop-filter | Any value other than none |
| perspective | Any value other than none |
| clip-path | Any value other than none |
| mask, mask-image, mask-border | Any value other than none |
| isolation: isolate | Made for exactly this purpose, no visual side effects |
| will-change | Naming any property that would create a stacking context when set |
| contain | layout, paint, or a shorthand that includes them (strict, content) |
| Top layer | Open modal dialog, popover, fullscreen element, and their ::backdrop |
The blue card is a context at z-index 1. Its tooltip has 9999 but is compared only with things inside the card, so the orange box at z-index 2 covers it.
<style>
.card { position: relative; z-index: 1; background: #bfdbfe; padding: 12px; width: 180px }
.tip { position: absolute; z-index: 9999; top: 40px; left: 60px; background: #1e3a8a; color: #fff; padding: 8px; width: 150px }
.other { position: relative; z-index: 2; background: #fdba74; margin-top: -10px; margin-left: 100px; padding: 24px; width: 140px }
</style>
<div class="card">Card (z 1)<div class="tip">Tooltip (z 9999)</div></div>
<div class="other">Other box (z 2)</div>Without isolate, the decoration with z-index: -1 would drop behind the section's background and disappear. With it, the section is its own context.
<style>
.hero { isolation: isolate; position: relative; background: #f1f5f9; padding: 24px; overflow: hidden }
.blob { position: absolute; z-index: -1; inset: 10px auto auto 10px; width: 120px; height: 80px; background: #a78bfa; border-radius: 50% }
</style>
<section class="hero"><div class="blob"></div><h2 style="margin:0">Text above the shape</h2></section>The header is sticky, so it is already a context. Give it a z-index above the content and the dropdown inside inherits that layer. No huge numbers needed.
:root { --z-dropdown: 100; --z-sticky: 200; }
.site-header {
position: sticky;
top: 0;
z-index: var(--z-sticky); /* above every section below */
background: #fff; /* so content does not show through */
}
.site-header .menu {
position: absolute;
top: 100%;
z-index: var(--z-dropdown); /* only competes inside the header */
}
/* Do not put transform or overflow: hidden on .site-header, or the menu gets clipped or trapped. */
Inside one stacking context the browser paints in a fixed order: the context's own background, then children with negative z-index, then normal block content, floats, inline content, positioned elements with z-index auto or 0 in source order, and finally positive z-index from low to high.
A child context is painted as a single unit at its parent's position in that list. Everything inside is sorted only against its siblings. That is the whole reason a 9999 inside a z-index: 1 card loses to a z-index: 2 neighbour.
First confirm it is a stacking problem: if the element is cut off at a straight edge, look for overflow on an ancestor instead. Then walk up from the hidden element in DevTools, checking each ancestor's computed styles for the triggers listed on this page.
Chromium DevTools has a Layers panel and Edge has a 3D View that show composited layers visually. Once you find the ancestor that forms the context, either raise that ancestor, remove the trigger, or render the floating piece outside it.
Either the element is not positioned (z-index needs position other than static, or a flex or grid parent), or it is trapped in a stacking context. The parent's position in its own context decides the result, and children cannot escape it, whatever number they carry.
The root element, positioned elements with a z-index, fixed and sticky elements, flex and grid children with a z-index, opacity below 1, transforms, filters, clip-path, masks, mix-blend-mode, perspective, isolation: isolate, some contain and will-change values, size containers and top layer elements.
It creates a new stacking context and does nothing else, with no visual change. Put it on a component root so negative z-index children stay behind the component's own background instead of falling behind the page, and so its internal layers never mix with outside ones.
Give the header or container holding the dropdown a higher z-index than the sections after it, both being positioned. If the container has a transform or opacity, remember that is its own context, so the z-index must go on that container, not on the dropdown.
It is valid, but the element goes behind its stacking context's background. Without a context on the parent, that can mean behind the page background and invisible. Add isolation: isolate on the parent to keep it inside.
Related: CSS position, CSS transforms, CSS variables, Modern CSS features, CSS specificity calculator.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.