Skip to content

Choosing an overlay

Six things in this set float above the page, and they look similar enough that picking the wrong one is easy. The difference is never how they are drawn — it is what they promise the reader.

Interrupts? Holds actions? Opens on
Dialog yes yes a deliberate click
ConfirmDialog yes, harder yes, one of them destructive a deliberate click
Drawer yes yes a deliberate click
Popover no yes a deliberate click
PreviewCard no no hover, after a pause
Tooltip no never hover or focus

Dialog stops the work. Everything behind it goes inert — not merely dimmed, but out of reach of the keyboard and of a screen reader — so use it when the answer is genuinely needed before anything else can happen.

ConfirmDialog is a Dialog for a choice that cannot be taken back. It announces itself as alertdialog, and clicking away does not dismiss it — an answer given by a stray click is exactly what it exists to prevent. Escape still closes it, deliberately: a popup with no keyboard exit is a trap, and pressing Escape is a decision rather than an accident. Reach for it when the action deletes, overwrites or sends.

Drawer is a Dialog that arrives from an edge. Same interruption, more room; it suits a panel of settings or a form that would look cramped in a box.

Popover floats next to what opened it and leaves the page alone. It can hold controls — a filter, a small form, a menu of options. If the reader can carry on without answering it, this is the one.

PreviewCard is what a link looks like before you follow it. It appears on hover after a pause, and it is not for anything the reader has to click: appearing on hover means it can vanish on the way to the thing inside it.

Tooltip names a control that is otherwise only an icon. It carries no interactive content at all — never a link, never a button — because there is no way to reach one with a keyboard. If you want to put a button inside a tooltip, you want a Popover.

The behaviour is Base UI’s, which means it is the same behaviour in each of them and it is the part that is hard to write:

  • focus moves into the overlay when it opens and returns to whatever opened it when it closes;
  • Escape closes what should be closeable;
  • a modal locks the scroll and marks the page behind it inert;
  • the popup is named by its own title through aria-labelledby, without anyone wiring up an id.

What is dowel’s is the clothes — and the enter and the leave, which are class lists rather than a state machine, because Base UI puts data-open, data-closed and data-starting-style on the element for you.

And one rule about size, which holds for every one of them and for menus, lists and toasts too: anything drawn over the page fits the window on both axes, and scrolls inside itself when there is more than fits. A popup placed against a trigger is held to the room its positioner says is left between the trigger and the window’s edges; a dialog, a drawer or a toast, to the window. The scroll is contained, so reaching the end of a long menu does not start scrolling the page underneath it.

A dialog, a drawer, a confirmation, the palette and a toast are columns, and only one part of each gives way — the body, the list, the description — so the title and the answer stay in reach while a long form moves between them. Put a long form in DialogBody, not straight into the popup.

The short example on the stand always fits, so the rule is not checked by looking at one: a gate hands every surface in every variant far more content than any window holds.

Overlays sit on named layers rather than on numbers picked in the moment:

--z-popup: 10; /* a popup in the flow */
--z-sticky: 20;
--z-menu: 30;
--z-floating: 40; /* Popover, PreviewCard */
--z-overlay: 50; /* the scrim */
--z-modal: 60; /* Dialog, ConfirmDialog, Drawer */
--z-palette: 70;
--z-toast: 80; /* has to clear everything */

A component reads these directly — [z-index:var(--z-modal)] — because Tailwind has no z-index namespace and z-50 would be a literal fifty with nothing to say about what it covers.

The ladder answers “who covers whom” for things on the page, and has nothing to say about a popup opened inside an overlay: a select in a dialog is a 30 beside a 60, a menu in a drawer the same, a select in a popover a 30 beside a 40. Each opens underneath the thing that asked for it.

So every overlay opens a layer for its content — a host inside its own portal where the popup rungs are raised one above the overlay, worked out by the stylesheet from the ladder — and every popup portals into the layer it was opened in. A select in a popover in a dialog climbs to 62 without anyone writing a number. Nothing to do at the call site; a product’s own full-screen stage takes LayerProvider above="overlay".