Skip to content
Back to Design Hub

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

FAQ