# Stacking (/docs/stacking)





Overlays paint in the order this page describes, and **there is nothing to set up** — no provider, no `zIndex` props, no `!important` anywhere in the chain. Read on only if you need to retune the order, or you're staring at a panel that's gone behind something.

## The four bands [#the-four-bands]

Every floating surface belongs to one of four bands, each a CSS variable on `:root`. Higher paints later, so it wins:

<BandLadder />

```css
:root {
  --overlay-z: 50;
  --toast-z: 100;
  --modal-z: 150;
  --tooltip-z: 200;
}
```

Two of those choices are worth spelling out:

* **`Tooltip` is on top** because it can appear over anything hoverable — including an already-open picker.
* **Toasts sit above page overlays but below dialogs**, so a `Modal` covers an arriving toast rather than the other way round.

Bands alone aren't enough though, because overlays nest. Two different mechanisms handle that, and which one applies depends on whether you crossed a band.

## Nesting in the same band [#nesting-in-the-same-band]

A `Select` opened inside a `Popover` — the shape of a [`DataTable`](/docs/data-table) column filter — is two panels from the *same* band, so their base numbers tie. Depth breaks the tie:

<LayerIndexStack />

```css
z-index: calc(var(--overlay-z, 50) + var(--layer-index, 0));
```

The result is that no component type needs its own tier. A picker doesn't outrank a popover because it's a picker; it outranks it because it opened later, on top of it.

<Preview align="center" code="&#x22;<Popover.Root width=\&#x22;auto\&#x22;>\n  <Popover.Trigger asChild><Button>Filter campus</Button></Popover.Trigger>\n  <Popover.Content>\n    <Popover.Body>\n      <Select data={operators} label=\&#x22;Operator\&#x22; />\n      <DatePicker label=\&#x22;Since\&#x22; />\n    </Popover.Body>\n  </Popover.Content>\n</Popover.Root>&#x22;">
  <SelectInPopover />
</Preview>

## Nesting across bands [#nesting-across-bands]

Now the harder case: a `Menu` opened from inside a `Drawer`. The menu is in the overlay band (50), the drawer in the modal band (150), so depth can't save it — `50 + 1` is still nowhere near 150.

Raising the overlay band above the modal band wouldn't fix it either; it would just break the reverse case, a `Modal` opened from a `Popover`. &#x2A;*No fixed pair of numbers is correct in both nesting directions.**

So this case isn't solved with numbers at all. `Modal` and `Drawer` publish their own positioner as the portal target for anything opened inside them, which makes the panel a *descendant* of a surface that already outranks the page:

<PortalNesting />

<Preview align="center" code="&#x22;<Drawer open={open} onOpenChange={setOpen} title=\&#x22;Account\&#x22;>\n  <Menu.Root>…</Menu.Root>   {/* portals into the Drawer, not <body> */}\n  <Select data={campuses} />\n</Drawer>&#x22;">
  <OverlaysInDrawer />
</Preview>

<Preview align="center" code="&#x22;<Modal open={open} onOpenChange={setOpen} title=\&#x22;New session\&#x22;>\n  <Select data={campuses} />\n  <DatePicker label=\&#x22;Starts on\&#x22; />\n</Modal>&#x22;">
  <PickersInModal />
</Preview>

This is also what lets `--toast-z` sit *between* the two bands: a picker inside a dialog never has to out-number a toast, because it isn't competing with one — it's inside the dialog that already covers it.

Override the target with `container`, or render in place with `withinPortal={false}`. Both are available on `Popover`, `Menu`, `Tooltip`, `HoverCard`, `Select`, `Combobox` and `DatePicker`.

### One outside click, one layer [#one-outside-click-one-layer]

Open the date picker above, then click the backdrop. The calendar closes and the modal stays open; a second click closes the modal. A `Modal`, `Drawer` or `Popover` won't dismiss on an outside click while a `Popover`, `Menu`, `Select`, `Combobox` or `DatePicker` opened inside it is still open, so a stack unwinds one layer per click. Without this, a stray click would discard the form the user was filling in along with the picker. This works wherever the panel portals to, even with `container` or `withinPortal={false}`. Tooltips and hover cards don't count, so a hover preview never costs the backdrop an extra click.

## Retuning a band [#retuning-a-band]

Set the variable in your own stylesheet. App chrome pinned high, for instance:

```css
:root {
  --modal-z: 200; /* clear a site header at 120 */
}
```

Because these are ordinary custom properties, they inherit — so you can scope a whole subtree, which is what you want for a widget embedded in someone else's page:

```css
.embedded-widget {
  --overlay-z: 10;
  --modal-z: 20;
}
```

Keep the relative order intact (`overlay < toast < modal < tooltip`) unless you specifically want to invert one of the two choices called out above.

## Debugging: where the number actually lives [#debugging-where-the-number-actually-lives]

If you go looking in devtools, the z-index will not be where you expect — and this is the one part worth understanding before you try to override anything.

Ark positions each panel with a `positioner` part, and `@zag-js/popper` puts two inline styles on it: `isolation: isolate`, which makes it *always* its own stacking context, and `z-index: var(--z-index)`. Then, on the first position pass, popper **writes `--z-index` inline itself**, copying the computed `z-index` of its content child:

<PopperMirror />

Two things follow, and they're the reason this kit declares the scale where it does:

* **Set `z-index` on content.** Popper hoists it onto the positioner for you, and that hoisted value is what competes across portals.
* **Don't set `--z-index` on the positioner.** Popper's own inline write replaces whatever your stylesheet said, the moment the panel positions. Only `!important` would beat it — if you find yourself reaching for that, you're on the wrong element.

One more trap in the same area. `data-scope` and `data-part` are emitted on the *same* element, so a selector needs both compounded — with a space it silently matches nothing:

```css
[data-scope='select'][data-part='content'] { … }  /* ✓ compound */
[data-scope='select'] [data-part='content'] { … } /* ✗ matches nothing */
```

That's not hypothetical: it's the bug this scale replaced. `Popover`, `Menu` and `Tooltip` don't render a root DOM node at all, so nothing can ever carry the scope as an ancestor.
