The CSS Z-Index Property
z-index property in CSS is essentially the "depth" control for your webpage. While x moves things left/right and y moves things up/down, z-index determines which elements sit on top when they overlap.For z-index to work, the element must have a position value other than the default static. It works with:
relative
absolute
fixed
sticky
(Newer standard): It also works on flex items and grid items, even if their position is static.
The property accepts three types of values:
| Value | Description |
auto |
(Default) The element’s stack order is the same as its parent. |
number |
Positive or negative integers (e.g., 1, 100, -5). Higher numbers sit closer to the user. |
inherit |
Takes the value from the parent element. |
The most common frustration with z-index is when a z-index: 9999 element still hides behind a z-index: 1 element. This happens because of Stacking Contexts.
Think of a stacking context like a folder.
If Folder A is ranked lower than Folder B, nothing inside Folder A can ever appear above Folder B, no matter how high its z-index is.
Setting position: absolute or relative with a z-index other than auto.
Setting opacity to less than 1.
Using transform, filter, or perspective properties.
Setting display: flex or grid on a parent with z-index on the child.
If no z-index is defined, the browser follows the Natural Stacking Order:
Background and Borders: Of the root element.
Non-positioned blocks: Elements in the normal flow.
Positioned elements: Elements with relative, absolute, etc., in the order they appear in the HTML (the "last one wins" rule).
Don’t use 9999: It’s tempting, but it leads to "z-index wars." Use a scale (e.g., 10, 20, 30) or CSS variables to manage levels.
Check the Parent: If an element won't come to the front, check if its parent has a limited stacking context or overflow: hidden.
The Negative Z-Index: Using z-index: -1 is great for placing decorative backgrounds behind text without adding extra wrapper divs.
Here is a practical demonstration. We will look at the "Parent Container Trap," which is the most common reason z-index fails in real-world projects.
In this scenario, even though the "Blue Box" has a massive z-index: 9999, it will stay behind the "Pink Box" because its parent (Folder A) is ranked lower than the Pink Box.
<div style="position: relative; z-index: 1; background: gray;">
<div class="blue-box" style="position: absolute; z-index: 9999; background: blue;">
I'm z-index 9999!
</div>
</div>
<div class="pink-box" style="position: relative; z-index: 2; background: pink;">
I'm only z-index 2, but I'm on top!
</div>
To fix this and allow the Blue Box to overlap the Pink Box, you have three main options:
Level the Playing Field: Remove the z-index from the parent (Folder A). This puts the Blue Box and the Pink Box in the same global stacking context.
Raise the Parent: Increase the parent's z-index (Folder A) to be higher than the Pink Box (e.g., z-index: 3).
The "Portal" Method: Move the Blue Box out of Folder A entirely so it sits at the bottom of your HTML <body>. This is why most Modals and Tooltips are appended to the end of the <body> tag.
| Scenario | Result | Why? |
| No z-index set | Last element in HTML is on top. | Natural DOM order. |
| Positioned vs. Static | Positioned always wins. | relative/absolute creates a new layer over static. |
| Negative z-index | Behind the parent's text. | Moves element to the very back of the current context. |
To keep your project organized and avoid "z-index wars" (where you end up with z-index: 9999999), the best practice is to use CSS Variables to create a centralized layer system.
This approach acts like a "Map" for your site's depth.
Add this to your :root or a global CSS file. By defining the order here, you can see at a glance how your site is layered.
CSS
:root {
/* Lowest layers */
--z-below: -1;
--z-default: 1;
/* Layout layers */
--z-dropdown: 100;
--z-sticky-nav: 200;
/* Interaction layers */
--z-modal-backdrop: 1000;
--z-modal-content: 1010;
/* Top-most layers */
--z-tooltip: 2000;
--z-toast-notification: 3000;
}
/* Usage in your components */
.main-nav {
position: sticky;
top: 0;
z-index: var(--z-sticky-nav);
}
.popup-modal {
position: fixed;
z-index: var(--z-modal-content);
}
Single Source of Truth: If you realize your Tooltips are appearing behind your Modals, you only have to change one line in your :root variables instead of searching through 20 different CSS files.
Readability: z-index: var(--z-modal-content) tells a developer exactly why that element is layered there, whereas z-index: 1010 is just a mystery number.
Scalability: You can easily "squeeze" a new layer in between (e.g., 105) without breaking the rest of the logic.
If you are ever unsure why an element is hidden, run this line in your Browser Console (F12):
JavaScript
// This lists all elements on the page that have a z-index set
[...document.querySelectorAll('*')]
.filter(el => getComputedStyle(el).zIndex !== 'auto')
.map(el => ({ element: el, zIndex: getComputedStyle(el).zIndex }));
The isolation: isolate property is the CSS version of "what happens in Vegas, stays in Vegas." It is a modern, clean way to create a stacking context without needing to hack the z-index or position of a parent element.
Usually, if you have a child element with z-index: -1, it tries to move behind its parent. But if the parent doesn't have its own stacking context, that child might accidentally slip behind the entire page background or other sections, making it disappear.
isolation: isolateWhen you apply isolation: isolate to a container, you are telling the browser: "Treat this container as a flat layer. Nothing inside can go behind or poke out in front of other elements on the page."
Imagine a "Hero" section with a background pattern. You want the pattern behind the text, but above the site's main background.
.hero-container {
isolation: isolate; /* Creates a clean boundary */
background: white;
}
.hero-pattern {
position: absolute;
z-index: -1; /* This stays INSIDE the hero, won't fall behind the site background */
}
isolation Instead of z-index: 0?While z-index: 0 (on a positioned element) also creates a stacking context, isolation is superior for two reasons:
No Position Required: You don't have to set position: relative just to fix a layering issue.
Cleaner Code: It explicitly tells other developers, "I am creating this boundary on purpose to manage internal layers."
If your z-index isn't working, check these 4 things in order:
Position: Is it static? (Change to relative, absolute, or fixed).
Parent Context: Is a parent "Folder" ranked lower than the target?
Opacity/Transform: Does a parent have opacity < 1 or a transform? (These create contexts automatically).
Isolation: Should you use isolation: isolate to keep your internal layers contained?
In Flexbox and Grid, the rules for z-index are much more developer-friendly. In a standard block layout, z-index is ignored unless you also add position: relative (or absolute/fixed).
However, Flex items and Grid items can use z-index even if their position is static.
In Flex and Grid, simply adding a z-index value (other than auto) to a child item automatically creates a stacking context for that child. You don't need to "activate" it with position: relative.
CSS
.flex-container {
display: flex;
}
.item-1 {
/* No position: relative needed! */
z-index: 2;
background: red;
margin-right: -20px; /* Overlap the next item */
}
.item-2 {
z-index: 1;
background: blue;
}
Flex and Grid also introduce the order property. It is important to know the difference:
order: Changes the visual position on the 2D plane (left/right or up/down).
z-index: Changes the stacking depth (front/back).
If two flex items overlap (due to negative margins or absolute positioning), the one with the higher z-index stays on top, regardless of its order value.
Grid is particularly powerful because you can assign multiple items to the same grid cell. Without z-index, the item defined last in your HTML will be on top. With z-index, you can easily toggle which one is visible.
CSS
.grid-container {
display: grid;
}
.image, .caption {
grid-column: 1 / 2;
grid-row: 1 / 2; /* Both items occupy the exact same space */
}
.caption {
z-index: 10; /* Sits directly on top of the image */
}
| Element Type | Position: Static | Position: Relative/Absolute |
| Standard Block | ❌ No | ✅ Yes |
| Flex Item | ✅ Yes | ✅ Yes |
| Grid Item | ✅ Yes | ✅ Yes |
If your z-index still isn't working inside a Flex or Grid container, check if the parent has opacity, transform, or filter applied. Those properties create a "wrapper" that can lock your items into a lower stacking level than the rest of the page.
This is a classic real-world challenge. To make a sticky header stay on top of a scrolling gallery, you have to balance position: sticky with a defined z-index to ensure it doesn't get "swallowed" by the gallery images as they slide up.
In this layout, the header needs to stay at the top of the viewport. Without a z-index, images in the gallery (especially those with their own transforms or positioning) might overlap the header.
CSS
/* 1. The Header */
.main-header {
position: sticky;
top: 0;
background: white;
height: 60px;
/* Elevate it above the rest of the content */
z-index: var(--z-sticky-nav); /* Using our Layer Manager from before! */
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
/* 2. The Gallery Container */
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
padding: 20px;
}
/* 3. The Gallery Images */
.gallery-item {
position: relative; /* Often used for hover effects */
overflow: hidden;
transition: transform 0.3s ease;
}
.gallery-item:hover {
/* Hovering often creates a new stacking context */
transform: scale(1.05);
z-index: 10;
}
position: sticky: Keeps the header at top: 0 as the user scrolls.
z-index on Header: Ensures that even if a gallery image has a z-index: 10 (like during a hover effect), the header's higher value (e.g., 200) keeps it safely on top.
The Stacking Context: By giving the .main-header a z-index, you've placed it in a higher layer than the .gallery container.
Parent Overflow: If any parent of your sticky header has overflow: hidden, overflow: auto, or overflow: scroll, the "stickiness" will break.
The "Sibling" Rule: If your header and your gallery are inside different parent containers, and those parents have their own z-index or opacity, the header might still hide behind the gallery. Always try to keep your global layout elements (Nav, Main, Footer) as direct children of the <body>.
The 3D View (or Layers panel) in modern browsers is the "secret weapon" for CSS developers. It allows you to rotate your website in 3D space to see exactly which elements are sitting on which "shelves."
In Chrome or Edge:
Right-click anywhere on your page and select Inspect (or press F12).
In the top-right corner of the DevTools panel, click the three vertical dots (⋮) or the "Plus" icon.
Select More Tools → 3D View (In some Chrome versions, it's under Layers).
Once the panel is open, select the "Z-index" tab. You will see your website rendered as a stack of planes:
Color Coding: Elements with a higher z-index are colored differently (usually darker or warmer colors).
Physical Depth: You can click and drag to rotate the view. Elements with a higher z-index will physically pop out toward you.
Nesting: You can visually identify if an element is "trapped" inside a parent's stacking context. If a child and parent are on the same flat plane, they share a context.
To wrap up everything we've covered, here is the ultimate checklist for mastering depth in CSS:
| Rule | The Logic |
| Position First | z-index ignored on static elements (except in Flex/Grid). |
| The "Folder" Rule | A child cannot break out of a parent's z-index or opacity. |
| Natural Order | If z-index is equal, the element lower in the HTML code wins. |
| Global Manager | Use CSS Variables (e.g., --z-modal) to avoid "9999" wars. |
| The Isolation Tool | Use isolation: isolate to create a clean boundary without positioning. |
The z-index property isn't just a number; it's a hierarchy. If you treat your CSS layers like a physical stack of paper rather than a competition of "who has the biggest number," your layouts will be much more stable and easier to debug.
Here is a complete, production-ready Z-Index Starter Template. You can drop this into any project to immediately bring order to your layout depth.
Place this at the very top of your CSS file. It acts as the "Master Map" for your project.
CSS
:root {
/* --- Low Level --- */
--z-below: -1;
--z-default: 1;
/* --- Layout & Components --- */
--z-card-overlay: 10;
--z-dropdown: 100;
--z-sticky-header: 200;
--z-floating-action-button: 300;
/* --- Overlays & System --- */
--z-modal-backdrop: 1000;
--z-modal-content: 1010;
--z-tooltip: 2000;
--z-toast: 3000;
--z-max: 999999; /* Use only for emergency global fixes */
}
These classes allow you to quickly solve layering issues without writing custom CSS for every single element.
CSS
/* Create a fresh stacking context without needing "position: relative" */
.is-isolated {
isolation: isolate;
}
/* Quick positioning utilities for z-index to actually work */
.pos-relative { position: relative; }
.pos-absolute { position: absolute; }
/* Layering Utilities */
.layer-top { z-index: var(--z-max); }
.layer-modal { z-index: var(--z-modal-content); }
.layer-header { z-index: var(--z-sticky-header); }
.layer-below { z-index: var(--z-below); }
Here is how you would use the template to build a "Hero" section where the text stays above a decorative background, but below a navigation bar.
HTML
<nav style="position: sticky; top: 0; z-index: var(--z-sticky-header); background: white;">
Logo & Menu
</nav>
<section class="hero is-isolated" style="position: relative; height: 400px;">
<div class="hero-content">
<h1>Welcome to the Site</h1>
</div>
<div class="hero-pattern" style="position: absolute; inset: 0; z-index: var(--z-below); background: url('pattern.svg');">
</div>
</section>
Stop using random numbers: If it’s not in your :root variables, don't use it.
Use isolation on sections: Apply .is-isolated to main sections (Hero, Footer, Sidebar) so their internal z-index values don't fight each other.
Inspect in 3D: If an element vanishes, use the browser's 3D View to see if it fell behind the "floor" of the page.