CSS z-index and Stacking Contexts: Layering Elements Correctly
CSS z-index controls which overlapping elements appear in front of others, but it only works the way you expect when you understand stacking contexts. This article explains how layering is decided, why z-index sometimes seems broken, and how to make overlays, menus, and positioned elements stack reliably.
Quick answer: z-index only affects positioned elements and elements that create their own stacking context. If two elements are in different stacking contexts, a larger number does not always win the way you expect.
Difficulty: Beginner
You'll understand this better if you know: basic CSS selectors, the position property, and how the document flow places elements on a page.
1. What Is z-index and a Stacking Context?
z-index is the CSS property that controls the visual layering of overlapping elements on the z axis, which is the imaginary depth direction of the page. A stacking context is the boundary that determines how elements are grouped and compared when the browser decides which item appears on top.
- z-index changes layer order only in a relevant stacking context.
- Higher values usually appear above lower values within the same context.
- Elements can create new stacking contexts, which isolate their children from the rest of the page.
- Layering is not global; it is often local to a parent element.
Think of stacking contexts like separate trays. Items in one tray can be ordered against each other, but they do not jump out and compete directly with items in another tray.
2. Why z-index Matters
Layering problems are common in real interfaces. Dropdowns can hide behind headers, tooltips can disappear under cards, and modal dialogs can sit behind page content if the stacking order is wrong.
z-index matters because it helps you control:
- menus and dropdowns above page content
- sticky headers above scrolling sections
- modal dialogs and backdrops
- badges and labels that must sit on top of images
- drag-and-drop previews and floating panels
You should use it when overlap matters. You do not need it for normal document flow where elements do not cover each other.
3. Basic Syntax or Core Idea
How z-index is written
The property accepts integer values such as 0, 1, 10, or negative values like -1. The browser compares these values inside the same stacking context.
.card {
position: relative;
z-index: 2;
}
.badge {
position: absolute;
z-index: 5;
}In this example, the badge can appear above other overlapping content inside the same context because it has a higher layer number.
What makes z-index work
In normal practice, z-index is used with positioned elements such as relative, absolute, fixed, or sticky. Without that, the property is often ignored in ways that confuse beginners.
Note: Modern CSS can also create stacking contexts through properties other than position, such as opacity less than 1, transform, filter, and isolation.
4. Step-by-Step Examples
Example 1: A simple overlap
This example shows two overlapping boxes. The one with the higher z-index appears on top.
.box-a {
position: relative;
z-index: 1;
background: #cce5ff;
}
.box-b {
position: relative;
z-index: 2;
background: #ffd8a8;
}Because .box-b has the larger value, it paints above .box-a when they overlap.
Example 2: Negative z-index
Negative values place an element behind other layered content inside the same stacking context. This is useful for decorative backgrounds that should stay behind content.
.decor {
position: absolute;
z-index: -1;
}
.panel {
position: relative;
z-index: 0;
}Use negative values carefully, because they can place content behind the parent background and make it seem to disappear.
Example 3: Stacking context created by opacity
An element with opacity less than 1 creates a new stacking context. That means children inside it cannot escape and overlap unrelated page content just by using larger numbers.
.fade-layer {
opacity: 0.9;
}
.popup {
position: absolute;
z-index: 9999;
}Even a very large value does not help if the popup is trapped inside a lower stacking context.
Example 4: Positioned header and modal
Fixed headers and modal dialogs are common uses for layering. A modal usually needs to sit above the header and the rest of the page.
.header {
position: fixed;
z-index: 100;
}
.modal {
position: fixed;
z-index: 1000;
}Using a higher stacking layer on the modal keeps it above the header when both overlap the viewport.
5. Practical Use Cases
- Place dropdown menus above surrounding content so they remain readable.
- Keep a sticky navigation bar above the page as the user scrolls.
- Build modal backdrops that cover the interface without blocking the dialog itself.
- Layer icon badges over avatars or product images.
- Create toast notifications that float above the main layout.
These are all cases where overlap is expected and layer control is part of the design.
6. Common Mistakes
Mistake 1: Using z-index without a positioned element
z-index is often ignored when the element is not positioned, so the value seems to do nothing. This is one of the most common reasons people think layering is broken.
Problem: The element has z-index, but without a positioning context it may not participate in layering the way you expect.
.tooltip {
z-index: 10;
}Fix: Add a positioning value that makes the element part of the stacking order.
.tooltip {
position: absolute;
z-index: 10;
}The corrected version works because the element now participates in layered positioning.
Mistake 2: Raising z-index in the wrong stacking context
A child cannot escape its parent stacking context just by using a bigger number. This is the classic reason for the complaint that z-index is not working.
Problem: The popup is inside a container that already forms a lower stacking context, so it stays below other page elements even with a large value.
.sidebar {
position: relative;
z-index: 1;
}
.sidebar .popup {
position: absolute;
z-index: 9999;
}Fix: Move the overlay to a parent context that can actually compete with the rest of the page, or adjust the parent layer itself.
.sidebar {
position: relative;
z-index: 20;
}
.popup {
position: absolute;
z-index: 1;
}This works because you are changing the layer of the whole context, not only the trapped child.
Mistake 3: Creating accidental stacking contexts
Some CSS properties create new stacking contexts even when that is not obvious. A transformed container, faded wrapper, or isolated element can block children from overlapping outside content.
Problem: The wrapper creates a new context, so the menu inside it cannot overlap the page header the way you expected.
.wrapper {
transform: translateY(0);
}
.menu {
position: absolute;
z-index: 999;
}Fix: Remove the property if it is unnecessary, or place the overlay outside the transformed parent.
.wrapper {
transform: none;
}
.menu {
position: fixed;
z-index: 1000;
}The corrected version works because the menu is no longer trapped inside an accidental layer boundary.
7. Best Practices
Practice 1: Use small, meaningful z-index values
Huge values like 9999 often hide a design problem instead of solving it. Small layer scales are easier to maintain and debug.
.base { z-index: 0; }
.dropdown { z-index: 10; }
.modal { z-index: 100; }A simple scale makes the order understandable without needing extreme numbers.
Practice 2: Define layering at component boundaries
Set the layer on the component that actually needs to sit above other content, not only on deeply nested children. This prevents surprises when contexts change.
.card {
position: relative;
z-index: 1;
}
.card .menu {
position: absolute;
}This approach keeps the stacking strategy aligned with the component hierarchy.
Practice 3: Watch for stacking context triggers
Properties like transform, opacity, and isolation can change layering behavior. Check parent elements first when something does not overlap as expected.
.panel {
position: relative;
isolation: isolate;
}This helps you create predictable stacking boundaries instead of accidental ones.
8. Limitations and Edge Cases
- z-index only compares elements inside the same stacking context.
- Auto values can follow source order when elements share the same layer level.
- Negative values may hide content behind a parent background.
- fixed and sticky elements can still be trapped by ancestor stacking contexts.
- Transforms and opacity can affect child layers even when the parent looks visually simple.
- Flex and grid items can participate in stacking order differently from normal block flow, which sometimes surprises developers.
If a layer seems invisible or ignored, inspect the ancestor chain. The problem is often not the target element itself.
9. Practical Mini Project
Here is a small layered card layout with a badge, image, and floating action button. It demonstrates a predictable stacking order from background to foreground.
.product-card {
position: relative;
width: 280px;
padding: 1rem;
background: white;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.product-image {
display: block;
width: 100%;
}
.badge {
position: absolute;
top: 0.75rem;
left: 0.75rem;
z-index: 2;
}
.cta {
position: absolute;
right: 1rem;
bottom: 1rem;
z-index: 3;
}In this mini project, the badge and button stay above the image because they are positioned and assigned explicit layer values. The card itself establishes the containing context, making the result easier to reason about.
10. Key Points
- z-index controls the order of overlapping elements.
- It works within stacking contexts, not across the entire page.
- Positioned elements are the most common candidates for layering.
- Unexpected stacking contexts are the main reason z-index seems broken.
- Small, consistent values are easier to maintain than very large numbers.
11. Practice Exercise
Create a simple notification panel with three layers: a page background, a card, and a close button that sits above everything inside the card. Make the button stay on top without using an extreme z-index value.
- The card should overlap the page background.
- The close button should overlap the card content.
- The layout should still behave correctly if you add more content inside the card.
Expected output: a card that appears above the page surface, with a close button clearly visible in the top-right corner of the card.
Hint: Give the card a positioning context, then layer the button relative to the card instead of the page.
Solution:
.page {
padding: 2rem;
background: #f5f7fb;
}
.notification {
position: relative;
z-index: 1;
max-width: 320px;
padding: 1rem;
background: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
}
.close-button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
z-index: 2;
}This solution works because the card and button use a local stacking relationship that is easy to predict and does not depend on the rest of the page.
12. Final Summary
z-index is the CSS tool for controlling overlap, but its behavior is governed by stacking contexts. That is why adding a bigger number does not always fix a layering problem. Once you understand which element creates the context, you can reason about the page in smaller, predictable layers.
For everyday work, remember the main pattern: position the element, inspect its ancestors, and keep your layer scale simple. Most mysterious layering bugs come from an unexpected parent context rather than from the element you are editing.
If you want to go further, practice debugging overlapping menus, headers, and modal dialogs in your own layouts. Those examples make stacking context behavior much easier to recognize in real projects.