In a large application, modals, dropdowns, and toasts keep escalating their z-index values to fight each other, and new overlays regularly appear behind existing chrome. How would you restructure the overlay layer so this stops recurring?
answer
- the number is the symptom, not the cause
- overlays should not paint where they are authored
- one mounting point makes them siblings
- a short named scale, centrally owned
- the top layer ends the competition
basics
~20 sStop letting overlays compete where they are authored. Mount them in one root-level container or the browser top layer, define a small ordered scale of named layers, and forbid arbitrary z-index values in components — the escalation is a structural problem, not a numbering one.
solid answer
~50 sThe escalation is a symptom: `z-index` only ranks elements within the same stacking context, so an overlay authored deep inside a page section can never reliably outrank chrome in a different branch, and raising the number appears to work until the next case. The structural fix has three parts. First, give overlays one mounting point — a container at the end of `<body>` — so their painting order and clipping stop depending on where the trigger happens to sit in the markup. Second, replace ad-hoc numbers with a small, ordered token scale (dropdown, sticky chrome, modal, toast) defined centrally as custom properties, so ordering becomes a design decision rather than an author's guess. Third, use the top layer via modal `<dialog>` and popovers for the overlays that genuinely must be above everything, which removes them from the competition entirely. Then enforce it: components may not set raw `z-index` values.
code
css · 11 lines:root {
--layer-dropdown: 100;
--layer-sticky: 200;
--layer-modal: 300;
--layer-toast: 400;
}
.dropdown { z-index: var(--layer-dropdown); }
.site-header { z-index: var(--layer-sticky); }
.modal { z-index: var(--layer-modal); }
.toast { z-index: var(--layer-toast); }go deeper
Know that huge z-index values are a warning sign rather than a solution, and that where an overlay lives in the document affects whether it can appear on top at all.
Be able to explain why the escalation happens and what a short, ordered token scale plus a single overlay container buys you over per-component numbers.
Walk through a concrete migration: introduce the root and tokens, collapse existing values onto the scale, convert modals to the top layer, and handle interleaving cases individually.
Own the policy and its costs — who owns the scale, how it is enforced in review or tooling, what flexibility teams give up, and which overlays deliberately stay out of the top layer.
## Read the symptom correctly "We keep raising `z-index`" is never really about numbers. It is a report that overlays are being authored in arbitrary places in the document, where their painting order depends on the structure of whatever page section happens to contain them. `z-index` orders elements only within a shared stacking context, so once an overlay is nested inside a subtree that participates in its own context, no value it sets can lift it past a sibling of that subtree. The first fix anyone tries — a bigger number — sometimes appears to work, which is exactly why the problem recurs: the team learns the wrong lesson and the values ratchet upward. ## Part one: one mounting point Overlays should not be painted where they are authored. Add a single container at the end of `<body>` and mount every floating layer into it. ```html <body> <div id="app">…</div> <div id="overlay-root"></div> </body> ``` This one move fixes three failure modes at once: an ancestor with clipped overflow can no longer cut the overlay off; an ancestor with a transform, filter, or containment can no longer capture a fixed overlay's containing block; and painting order stops being a function of where in the page the trigger lives. Everything mounted there is a sibling of everything else mounted there, which is the precondition for a numeric scale to mean anything at all. ## Part two: a small ordered scale With overlays as siblings, ordering becomes a design decision worth writing down. Define a handful of named levels centrally and let nothing else set the property: ```css :root { --layer-base: 0; --layer-dropdown: 100; --layer-sticky: 200; --layer-modal: 300; --layer-toast: 400; } .dropdown { z-index: var(--layer-dropdown); } .modal { z-index: var(--layer-modal); } ``` The gaps between values are deliberate room for later insertion; the important property is that the list is short, ordered, and reviewable. When someone needs a new level, the question is "where does this belong relative to the others", which is answerable, instead of "what number beats the current maximum", which is not. `isolation: isolate` is the companion tool: applying it to a self-contained widget guarantees its internal stacking cannot leak into the global ordering. ## Part three: retire the competition where you can For overlays that must genuinely be above all page content, the top layer removes the problem rather than managing it. A modal `<dialog>`, or an element shown as a popover, is painted above the entire document irrespective of any `z-index` in the page, is not clipped by ancestor overflow, and is positioned against the viewport. There is nothing left to escalate. It comes with a `::backdrop` for the scrim, which also deletes the hand-written overlay div and its own layering question. Adopt it for modals first — they are the strongest case and the least ambiguous — and keep the scale for the layers that must interleave with page content, such as dropdowns that should sit under a sticky header. ## Part four: make it stick A convention that is not enforced decays within a quarter. The mechanisms that hold: overlays are only creatable through a shared primitive that owns mounting and layering, so a product component never writes `position: fixed` plus a number itself; lint or review rules reject raw `z-index` literals outside the token file; and the tokens live in one file whose diff a reviewer will actually read. It helps to give the scale a documented owner, because the failure mode is not one bad decision but a hundred uncoordinated small ones. ## Sequencing a migration Do not attempt a big-bang rewrite. Introduce the tokens and the overlay root first and map the existing values onto the scale — most codebases collapse into four or five real levels, and the enormous numbers turn out to be duplicates of each other. Convert modals to `<dialog>` next, since they are self-contained and the win is largest. Leave the awkward interleaving cases (a dropdown inside a sticky toolbar inside a scrolling panel) until last and treat each as a design question about which thing should visually win, because that is what it actually is. ## What to say about the tradeoffs Centralised layering costs flexibility: a team that wants a bespoke ordering for one screen now has to negotiate. That is the point, but it should be stated honestly. The top layer's all-or-nothing painting is likewise a constraint, unsuitable for an overlay that must sit below some chrome. And a shared overlay root moves overlays away from their triggers, which is why positioning them deliberately — with anchor positioning where supported, or a positioning utility otherwise — becomes part of the same piece of work rather than an afterthought.
- Why does a single overlay root have to come before the layer tokens, rather than after?Because numbers only compare within a shared stacking context. While overlays are scattered through the page, two components with different tokens can still paint in the wrong order, and the scale gets blamed. Making them siblings under one root is what turns the tokens into a reliable ordering rather than a hopeful one.
- When would you keep a portal-and-token overlay instead of moving a component to the top layer?When the overlay must interleave — a dropdown that should appear under a sticky header, or a panel that sits above content but below a global banner. The top layer is deliberately all-or-nothing about painting above the document, and it also brings modality behaviour that is wrong for lightweight, non-blocking layers.
- How do you stop the convention eroding once the migration is done?Make the correct path the easy one: a shared overlay primitive that owns mounting and layering so product code never writes the property, a lint or review rule rejecting raw `z-index` literals outside the token file, and a named owner for the scale. Conventions that rely on memory regress within a release cycle.
saying these in an interview costs you the question
- Proposes a bigger maximum z-index as the fix
- Treats it as a naming problem rather than a structural one
- Gives every component its own arbitrary z-index value
- Assumes the top layer suits every overlay
- Plans a big-bang rewrite with no migration order