Z-Index Management & Stacking
Tokenized z-index scale and stacking contexts to end the z-index arms race.
Responsive DesignIntermediate
Why
z-index: 9999 sprinkled everywhere is a race: every new overlay needs a bigger number, and a modal ends up behind a tooltip. A token scale plus stacking-context awareness makes layering predictable.
StatCounter 2024 data shows 58% of web traffic is mobile. CSS container queries (baseline 2023) and subgrid (baseline 2024) eliminate most media-query hacks, reducing responsive CSS by 30-50%.
When
Adopt a z-index scale once you have 3+ overlapping layers (sticky header, dropdown, modal, toast). Don't wait for the bug where a tooltip hides behind a header.
How
1. Token scale — --z-base: 0, --z-dropdown: 100, --z-sticky: 200, --z-modal: 300, --z-toast: 400
2. Steps of 100 — leaves room to insert a layer (dropdown above sticky but below modal) without renumbering
3. Stacking context — any positioned element with z-index, OR transform/filter/opacity<1, creates one
4. Isolate — isolation: isolate on a section creates a fresh stacking context, contains its z-index
5. Portal — render modals/toasts in a portal at body end so they escape ancestor stacking contexts
6. Within a context — children only compete with siblings in the same context, not the whole page
7. Enforce — stylelint-declaration-strict-value bans raw z-index, only var(--z-*) allowed
Example
:root {
--z-base: 0;
--z-dropdown: 100;
--z-sticky: 200;
--z-drawer: 300;
--z-modal: 400;
--z-toast: 500;
}
.header { position: sticky; top: 0; z-index: var(--z-sticky); }
.dropdown{ position: absolute; z-index: var(--z-dropdown); }
.modal { position: fixed; z-index: var(--z-modal); } /* render in portal */
.toast { position: fixed; z-index: var(--z-toast); } /* render in portal */
/* Isolate a widget so its internal z-index never leaks out */
.widget { isolation: isolate; position: relative; }
/* stylelint — ban raw z-index */
"declaration-property-value-allowed-list": {
"z-index": ["/var\\(--z-/"]
}
Common Problems
- ⚠Magic numbers — z-index: 9999 then 99999 then 999999, no one knows what wins
- ⚠Modal behind a sticky header — modal's z-index is lower because it's in a nested stacking context
- ⚠opacity: 0.99 to 'fix' layering — accidentally creates a stacking context that traps children
Best Practices
- ✓Use a token scale with steps of 100 — room to insert layers without renumbering
- ✓Render top-level overlays in a portal — escapes ancestor stacking contexts entirely
- ✓Use isolation: isolate to contain a widget's z-index — its internals can't leak out