Suggest an editImprove this articleRefine the answer for “CSS z-index and stacking context”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**CSS z-index** sets the stacking order of positioned elements along the z-axis. Higher values appear in front, but only within the same stacking context. ```css .parent { position: relative; z-index: 1; } /* Stacking context */ .child { position: absolute; z-index: 100; } /* Trapped inside parent */ .other { position: relative; z-index: 2; } /* Beats parent, beats child */ ``` **Key point:** a child cannot escape its parent's stacking context. A child with z-index: 100 loses to a sibling with z-index: 2 if their parents are in different contexts.Shown above the full answer for quick recall.Answer (EN)Image**CSS z-index** - sets the stacking order of positioned elements along the z-axis, but that comparison only happens within a stacking context, a local container that isolates its children's z-index values from the rest of the page. ## Theory ### TL;DR - z-index only compares elements that share the same stacking context, not the whole page - A child with z-index: 9999 inside a z-index: 1 parent loses to any element with z-index: 2 outside that parent - Stacking contexts are created by `position` + non-auto `z-index`, `opacity < 1`, `transform`, `filter`, `isolation: isolate`, and a few others - Think of them as glass elevator shafts: elements inside one shaft never directly overlap elements in another shaft - Fix z-index problems with `isolation: isolate`, not by adding bigger numbers ### Quick Example ```css .parent { position: relative; z-index: 5; } /* Creates a stacking context */ .child { position: absolute; z-index: 100; } /* Trapped inside parent */ .other { position: absolute; z-index: 6; } /* Wins — compares to parent z=5, not child z=100 */ ``` The child has z-index: 100 but still sits below `.other`. The browser compares `.parent` (z=5) against `.other` (z=6). The child's value does not enter that race. It only competes inside `.parent`. ### How Stacking Contexts Work When the browser paints the page, it sorts elements by the z-order of their stacking context root, not by every individual z-index. Each context is painted as a flat group. The browser does not look inside. A context with z-index: 1 is fully rendered before a context with z-index: 2, regardless of what individual values exist inside either one. The root `<html>` element starts the first stacking context. Properties like `position: relative; z-index: 1` trigger new ones recursively. Chrome (Blink engine) tracks each context as a `PaintLayer` object for GPU compositing. ### What Creates a Stacking Context | Property / Condition | Example | | --- | --- | | Root element | `<html>` | | `position` + `z-index` (not auto) | `position: relative; z-index: 1` | | `opacity` < 1 | `opacity: 0.99` | | `transform` (any value) | `transform: translateX(0)` | | `filter` (any value) | `filter: blur(0)` | | `will-change` | `will-change: transform` | | `isolation: isolate` | Creates a context explicitly | | `position: fixed` or `sticky` | Always creates a context | | Flex/grid child with `z-index` | `z-index: 1` on a flex child | The most common surprise is `opacity: 0.99`. Set it for a subtle fade and it quietly creates a stacking context. Suddenly a tooltip inside disappears behind a sidebar with no obvious reason. ### When to Use - **Modal or drawer**: `position: fixed; z-index: 1000` at the root level. In React, use `ReactDOM.createPortal(modal, document.body)` to escape any parent context. - **Nested dropdown**: apply `isolation: isolate` to the parent component so inner z-index values do not affect elements outside. - **Overlapping cards**: `position: relative; z-index: auto` on siblings works fine without creating new contexts. - **Avoid large numbers**: instead of `z-index: 99999`, define a CSS custom property scale. ```css :root { --z-dropdown: 100; --z-modal: 400; --z-tooltip: 600; } .modal { position: fixed; z-index: var(--z-modal); } .tooltip { position: fixed; z-index: var(--z-tooltip); } ``` ### Common Mistakes **z-index on a static element** ```css .box { z-index: 10; } /* Has no effect */ ``` z-index is ignored unless `position` is `relative`, `absolute`, `fixed`, or `sticky`, or the element is a flex/grid child. Add `position: relative` and it works. **High z-index trapped in a low context** ```css .parent { opacity: 0.99; } /* Creates a stacking context */ .tooltip { position: absolute; z-index: 9999; } /* Stuck inside parent */ ``` `opacity` triggers a new context. Anything outside with z-index: 1 beats the tooltip. Move it to a root-level container or raise the parent's z-index. The transform trap is the bug that takes longest to find in real projects. You check the z-index, raise it, check again, and nothing changes, because the problem is three levels up in the DOM. **Negative z-index hides behind the background** ```css .overlay { position: absolute; z-index: -1; } ``` Negative values place elements below the parent's background. That is rarely the intent. Use `isolation: isolate` on the parent if you need to clip a layer without z-index tricks. **Flex order vs. z-index confusion** ```css .container { display: flex; } .item1 { order: 2; z-index: 10; position: relative; } .item2 { order: 1; z-index: 1; position: relative; transform: translateX(0); } ``` `.item2` ends up on top despite z-index: 1. The `transform` promotes it to its own compositing layer. Developers expect DOM order and visual order to match cleanly. They do not when compositing layers are involved. ### Real-World Usage - **React Portal**: `ReactDOM.createPortal(modal, document.body)` places the modal in the root stacking context, bypassing any parent traps - **TailwindCSS**: the `z-50` utility on `fixed` elements in Next.js apps follows this same model - **Bootstrap**: `.modal-backdrop` uses `z-index: 1040` inside a new context created by `opacity: 0.5` on the backdrop element - **Modern alternative**: the `::backdrop` pseudo-element and the Popover API handle overlays without manual z-index stacking in browsers that support them ### Follow-up Questions **Q:** What is the difference between `z-index: auto` and `z-index: 0`? **A:** `auto` means the element participates in its parent's stacking context without creating a new one. `0` creates a new stacking context at z-level zero, which changes how its children stack. **Q:** My modal has z-index: 9999 but it hides behind the navbar. Why? **A:** The navbar or one of its ancestors likely has `transform` or `filter`, which creates a stacking context that sits higher. Inspect computed styles on every ancestor up to `<body>` and look for those properties. **Q:** How does `will-change: transform` affect stacking? **A:** It promotes the element to a GPU compositing layer and creates a stacking context before the animation starts. This prevents jank during transitions but can cause layout surprises if you forget the context was created ahead of time. **Q:** Is there a case where `z-index: auto` and `z-index: 0` behave identically? **A:** Yes, when the element is not positioned and is not a flex or grid child. In that case, neither value does anything. The difference only appears when the element would otherwise create a stacking context. **Q:** (Senior) Does z-index work across Shadow DOM boundaries? **A:** No. Shadow roots create stacking contexts, and z-index does not pierce the shadow boundary. For cross-boundary stacking in Chrome 89+ you can use `::part()` or slotted elements to expose specific parts of a component. ## Examples ### Basic stacking order ```css .card-a { position: relative; z-index: 1; background: lightblue; } .card-b { position: relative; z-index: 2; /* Appears above card-a */ background: coral; } ``` Two siblings, both `position: relative`, both in the root stacking context. Their z-index values compare directly. The higher value wins. This is the simple case that works as expected. ### Modal over a dashboard card (React Portal) ```jsx // Without the portal, the modal is trapped in the parent's z-index: 1 context function Dashboard() { return ( <div style={{ position: 'relative', zIndex: 1 }}> <Card style={{ position: 'relative', zIndex: 10 }}> Dashboard card </Card> {ReactDOM.createPortal( <div style={{ position: 'fixed', inset: 0, zIndex: 1000 }}> Modal content {/* Now in root context, wins over everything */} </div>, document.body )} </div> ); } ``` Without the portal, the modal sits inside the `div` with `z-index: 1`. Even at `z-index: 1000`, it cannot beat a sibling with `z-index: 2` outside. The portal moves it to `document.body`, placing it in the root stacking context where the comparison is straightforward. ### The transform trap ```html <div class="sidebar" style="position: relative; z-index: 10; transform: translateX(0);"> <!-- transform creates a stacking context here --> <div class="tooltip" style="position: absolute; z-index: 9999;"> This tooltip is trapped inside sidebar </div> </div> <div class="header" style="position: relative; z-index: 11;"> Header beats the tooltip despite tooltip z=9999 </div> ``` The sidebar's `transform` creates a stacking context. The tooltip's z-index: 9999 only counts inside the sidebar. The header compares against the sidebar (z=10), not the tooltip, and wins with z=11. Remove `transform` from the sidebar or increase the sidebar's z-index to fix it.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.