CSS z-index
How z-index Works
z-index controls the stacking order of elements that overlap. Higher values appear on top. But there are two critical rules most people miss:
- z-index only works on positioned elements — the element must have
position: relative,absolute,fixed, orsticky. Onposition: static(the default), z-index is completely ignored. - z-index is scoped to stacking contexts — elements can only compete with siblings in the same stacking context. A child can never appear above or below elements outside its parent’s stacking context.
Basic Example
Stacking Contexts Explained
A stacking context is like a z-index scope. Elements inside a stacking context are layered relative to each other, but the entire context is treated as a single unit in the parent’s stacking order.
In this example, .child-a has z-index 999 but still appears below .child-b (z-index 1). Why? Because .parent-a (z-index 1) is below .parent-b (z-index 2). The child cannot escape its parent’s stacking context.
What Creates a Stacking Context
Many CSS properties create new stacking contexts (not just z-index):
position: relative/absolute/fixed/stickywithz-indexother thanautoopacityless than 1transformwith any value other thannonefilter,backdrop-filterperspectiveclip-pathisolation: isolatewill-change: transformorwill-change: opacity- Flex/grid children with
z-indexset (even without explicit positioning) contain: layoutorcontain: paint
This is why adding opacity: 0.99 or transform: translateZ(0) can “fix” z-index issues — they create a new stacking context.
z-index Scale for Your Project
Define a scale and stick to it. This prevents z-index wars where numbers keep escalating to 99999:
| Layer | z-index | Examples |
|---|---|---|
| Base | 0 | Page content |
| Elevated | 1 | Cards, raised elements |
| Dropdown | 10 | Menus, popovers, tooltips |
| Sticky | 100 | Fixed headers, sticky sidebars |
| Overlay | 1000 | Modals, drawers, lightboxes |
| Toast | 10000 | Notifications, error banners |
isolation: isolate
Creates a stacking context without any visual side effects. Use it to scope z-index within a component:
This is the cleanest way to create a stacking context. No position, no opacity hacks.
Debugging z-index
- Open DevTools, select the element, check if
positionis set - Walk up the DOM tree — find which ancestors create stacking contexts
- Chrome DevTools: in the Layers panel, you can see all stacking contexts visually
- If an element “won’t go above” something, its parent’s stacking context is likely too low
Frequently Asked Questions
position: static, which ignores z-index. Add position: relative. (2) The element is inside a stacking context with a lower z-index than the element you are trying to cover. No child z-index can escape its parent’s stacking context.opacity < 1, transform, filter, clip-path, isolation: isolate, will-change, and flex/grid children with z-index. This is why adding transform: translateZ(0) can fix z-index issues.