Z-index layers
Every stacking level in the app, what lives there, and the rule for adding one
Read this before inventing a z-index. Almost everything belongs in a layer that already exists.
| Layer | Class | What lives there |
|---|---|---|
| Ground | z-0 | Explicit stacking contexts — a card that needs to contain its own absolutes |
| Inline overlay | z-10 | Gradients, dividers, upload controls, focus rings, scroll buttons, drag states. The default for "on top of the thing next to it" |
| Sticky in-page | z-20 | Filter bars, carousel indicators, sidebar resize handle |
| Gallery overlay | z-30–z-32 | The mobile item-gallery controls (back, favorite, counter) over a full-bleed image |
| Header | z-40 | The sticky site header |
| Onboarding helper | z-45 | The seller onboarding widget — above the header, below the popup layer |
| Popups | z-50 | Dialog, sheet, drawer, alert dialog, dropdown, popover, select, context menu, sticky bottom bar. The shadcn default |
Everything Base UI portals shares z-50. Popups do not outrank each other by z-index: a popup opened from inside another (a select in a dialog, a dialog nested in a sheet) portals into its parent's portal node, so it paints above it by DOM order. Two dialogs that must stack are Base UI nested dialogs, the child's Root rendered inside the parent's popup; the first-visit gate (the market door nested in the cookie banner) is the example.
Above that there are only escape hatches, and each has exactly one owner: the PhotoSwipe lightbox sets its own root via --pswp-root-z-index, and dev-only surfaces (debug panels, the email preview shell) sit at z-9999 and above where nothing product-facing can collide with them.
The rule
The ladder is about containment, not importance. A layer exists because something inside it must escape the box it was rendered in. If your component does not escape its box, it does not need a z-index — it needs a stacking context on its parent.
Before adding a value:
- Can it live at
z-10? Most things can. - Is it a variant of a layer that exists? Use that layer's class, don't add a neighbour.
- Genuinely new? Add it to the table above in the same commit, with the reason.
Arbitrary values (z-[65]) are how the ladder rots. Two components a point apart is a bug waiting for the first browser that resolves ties differently.