Topic 6.3
z-index and Stacking Contexts
In one line
When boxes overlap, the browser paints them in a fixed order; z-index changes that order for positioned boxes, but only among siblings in the same stacking context, which is why a child with z-index: 9999 can still be stuck under another element.
Think of it like this
Sheets of paper in folders on a desk. Inside one folder you can shuffle the sheets in any order you like: sheet 9999 goes on top of that folder. But if the whole folder is under another folder, every sheet in it is under that other folder too, however high its number. A stacking context is a folder. z-index only shuffles things inside the same folder.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- z-axis
- The direction pointing out of the screen towards you. Boxes higher on it are drawn on top.
- Painting order
- The order in which the browser draws boxes. Whatever is drawn last appears on top.
- `z-index`
- A number that moves a positioned box (or flex/grid item) up or down the painting order among its siblings.
- Stacking context
- A group of boxes painted together as one layer.
z-indexvalues inside it only compete with each other. - Root stacking context
- The first stacking context, created by the
<html>element. Everything starts inside it. - `isolation: isolate`
- A property whose only job is to create a new stacking context.
- Flex item / grid item
- A direct child of an element with
display: flexordisplay: grid.z-indexworks on it without positioning. - Top layer
- A special layer above the whole page, used by modal
<dialog>s, popovers and full-screen elements. Noz-indexcan beat it.
Step by step
01Overlap without z-index
Two absolutely positioned boxes overlap. Neither has a z-index, so the one later in the HTML is painted later and appears on top. A non-positioned box overlapping them would be underneath both.
<div class="box red"></div> <!-- painted first -->
<div class="box blue"></div> <!-- painted later: on top -->02Raise one with z-index
Give the red box z-index: 1. Positive z-index boxes are painted after every auto/0 box in the same context, so red now covers blue.
.red {
position: absolute;
z-index: 1;
}03See the folders
A header with position: relative; z-index: 1 is a stacking context. Its menu's z-index: 999 only counts inside the header. The banner below, with z-index: 2, is compared with the header (1), not the menu, so it covers the menu.
04Fix it at the right level
Either remove z-index from the header (it stops being a context, so the menu's 999 competes in the root context), or raise the header above the banner (z-index: 3). Raising the menu further never helps.
.header {
position: relative;
z-index: 3; /* compete with the banner at the level where they meet */
}05Spot the hidden context makers
A card fades in with opacity: 0.99 or slides in with transform. Both create a stacking context, so a tooltip inside the card is now trapped under the next card. Check every ancestor for these properties in DevTools' Computed tab.
.card {
opacity: 0.95; /* creates a stacking context */
transform: scale(1); /* so does this */
will-change: transform; /* and this */
}06Use isolation and a z-index scale
Give components isolation: isolate so their internal z-index values stay inside them, then keep a short, named scale for the page-level layers, so nobody needs 9999.
:root {
--z-dropdown: 10;
--z-sticky: 20;
--z-overlay: 30;
--z-toast: 40;
}
.card { isolation: isolate; }
.site-header { position: sticky; top: 0; z-index: var(--z-sticky); }Try it yourself
- 1
Remove a z-index
In the first example, delete
z-index: 3from#a. Predict the three console lines before running. (At (60, 40)bnow wins because it is later in the HTML; at (100, 70)cwins with itsz-index: 1.) - 2
Fix the bug the other way
In the second example, delete the two script lines that change the header, and instead change the header's
z-indexto3in the CSS. Predict the first console line. (The menu wins, because now the whole header layer is above the banner.) - 3
Create a context by accident
In the second example's CSS, change the header to
z-index: autobut addopacity: 0.99. Predict who is on top at (60, 70) before the script runs. (The banner:opacitybelow 1 creates a stacking context, and the banner's positive z-index is painted after it.)
Code & diagrams
`elementFromPoint(x, y)` returns the top element at a point of the page, so it tells us who won. `b` comes after `a` in the HTML but `a` has `z-index: 3`. `c` (z-index 1) beats `b` (auto). Each box's `opacity: 0.9` makes it a stacking context, which is harmless here because the boxes have no children that need to escape.
Preview
Console
Nothing logged yet.Expected console output
(60, 40) a and b overlap -> a
(140, 90) b and c overlap -> c
(100, 70) all three overlap -> aThe point (60, 70) is where the menu hangs over the banner. While the header has `z-index: 1` it is a stacking context and the banner (2) covers it, menu and all. The script sets the header to `auto`: it stops being a stacking context, and the menu's 999 now competes directly with the banner's 2.
Preview
Console
Nothing logged yet.Expected console output
header z-index 1 -> banner
header z-index auto -> menuWith `z-index: -1` the circle is painted in the root context's negative layer, which comes before normal blocks, so the panel's white background hides it. The script moves the circle into the panel and gives the panel `isolation: isolate`. Now the panel is its own stacking context: its background is painted first, then its negative children, so the circle shows above the white but stays behind the panel's text.
Preview
Console
Nothing logged yet.Expected console output
before: panel
inside an isolated panel: glowBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
z-index on a static element
Give a non-positioned <div class="popup"> (not a flex or grid item) z-index: 100 to bring it above a positioned sidebar.
Break #2
A tooltip trapped by a fade-in animation
Add a hover effect .card:hover { transform: translateY(-2px); } to a card whose tooltip is meant to overlap the next card.
Myth vs fact
Myth
A higher z-index is always on top.
Fact
Only among boxes in the same stacking context. A child can never rise above something its stacking-context ancestor is below.
Myth
Use z-index: 9999 to be safe.
Fact
Huge numbers don't escape stacking contexts and start an arms race. Use a small named scale and isolation: isolate on components.
Myth
z-index needs position to work.
Fact
It also works on flex and grid items with no position set.
Myth
A modal needs the highest z-index on the page.
Fact
A <dialog> opened with showModal() and elements using the popover attribute go into the top layer, which is above every z-index in the document.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The painting order comes from CSS 2.1 Appendix E. Note the subtle part: non-positioned elements with
opacity,transformand the like are painted at step 6 as if they were positioned withz-index: 0. That's why addingopacity: 0.99to a static element can suddenly put it above a neighbour. - ▸
Stacking contexts are a painting concept, and browsers often (not always) give elements with
transform,will-changeor animatedopacitytheir own compositor layers. Too many layers cost GPU memory. Chrome DevTools' Layers panel shows them; don't addwill-changeeverywhere "for performance". - ▸
z-indexis not inherited, and its computed value for a non-positioned, non-flex/grid element is still whatever you wrote; it is only ignored at paint time. Reading it in JavaScript tells you nothing about whether it works. - ▸
Hit-testing (which element receives a click) follows the same painting order, so a covered element also can't be clicked.
pointer-events: noneon the cover lets clicks pass through, which is useful for decorative overlays.
Remember this
- 1
The z-axis is the imaginary line pointing out of the screen towards you. When boxes overlap, the browser must decide which one is drawn last, so it appears on top. Without any
z-index, it uses the painting order from the spec. Inside one stacking context, from bottom to top: (1) the context's own background and border, (2) children with negativez-index, (3) normal in-flow block boxes, (4) floats, (5) inline content such as text, (6) positioned boxes withz-index: autoor0, in source order, (7) children with positivez-index, lowest first. - 2
So without
z-index, a positioned box (relative, absolute, fixed, sticky) is painted above non-positioned boxes, and among positioned boxes the one later in the HTML is on top. That's why a dropdown written early in the page can be covered by a positioned card written later. - 3
z-indextakes an integer (negative, zero or positive) orauto. It works on positioned elements and on flex items and grid items (children ofdisplay: flexordisplay: gridparents), even when they are not positioned. On any other box it does nothing. A bigger number is painted later, so it appears on top, but only compared with other boxes in the same stacking context. - 4
A stacking context is a group of boxes that is stacked as one unit inside its parent context. Everything inside it is painted together, then the whole group is placed in its parent's order using the group root's own
z-index. Children can never escape it: a child withz-index: 9999inside a context withz-index: 1still sits below a sibling context withz-index: 2. The root<html>element creates the first one. - 5
Many properties create a new stacking context:
position: relativeorabsolutewith az-indexother thanauto;position: fixedorsticky(always); a flex or grid item with az-indexother thanauto;opacitybelow1;transform,filter,backdrop-filter,perspective,clip-pathormaskother thannone;mix-blend-modeother thannormal;isolation: isolate;contain: layoutorpaint; andwill-changenaming any of these. Many stacking bugs come from one of these added for a different reason, such as an animation or a fade. - 6
Debugging "z-index doesn't work": (1) check that the element is positioned or a flex/grid item, otherwise
z-indexis ignored; (2) walk up its ancestors and find the nearest one that creates a stacking context; (3) compare that ancestor with the element that covers yours, at the level where they are siblings in the same context. Fix it there: raise the ancestor'sz-index, remove the property that created the unwanted context, or move the element (a menu or modal) higher in the HTML.isolation: isolateis the clean way to create a context on purpose, with no other visual effect.
Explain it without notes
What is a stacking context, and how does it limit z-index?
List several CSS properties that create a stacking context.
Describe the default painting order inside one stacking context.
A dropdown has z-index: 9999 but appears under a banner with z-index: 2. How do you debug and fix it?
Practice
Make a .card whose decorative ::before blob sits behind the card's text but above the page background, using a negative z-index.
Two absolutely positioned boxes .first and .second overlap; .second is later in the HTML. Make .first appear on top without changing the HTML order.
A .site-header with position: sticky should stay above every card on the page, including cards with hover transforms. Write the CSS.
Trade-offs
- ↔
isolation: isolateon components keeps their internal layers from leaking, but it also stops any child from rising above neighbouring components; tooltips and menus then need to live outside the component or in the top layer. - ↔
A named
z-indexscale in custom properties keeps layers predictable across a team, at the cost of a little upfront planning and discipline to never use magic numbers.
Done when you can
I can say which elements
z-indexapplies to.I can list the main properties that create a stacking context.
I can explain why a child with a huge
z-indexcan stay under another element.I can debug a z-index bug by finding the stacking context ancestor.
I can use
isolation: isolateand a negativez-indexsafely.