Close only the top layer
An outside click closes the portalled popover, not the modal behind it.
Emil Kowalski@emilkowalskiWhen you open a popover inside a modal (think a custom select), clicking outside the popover to close it also closes the modal. The desired behavior is to close the select on click outside while keeping the modal open.
From emil-design-eng.
One modal, two dismissal rules
Type a client, open Terms, then click the backdrop.
Each layer checks DOMBad
lost Top layer dismissesGood
lost // One stack of open layers. Only the top one answers an outside click or Esc.
const layers = [];
function dismissable(el, { owns = [], onDismiss }) {
const layer = { el, owns, onDismiss };
layers.push(layer);
return () => layers.splice(layers.indexOf(layer), 1); // call on close
}
document.addEventListener("pointerdown", (e) => {
const top = layers.at(-1);
if (!top) return;
const inside = [top.el, ...top.owns].some((n) => n.contains(e.target));
if (!inside) top.onDismiss(); // the modal underneath never sees this click
}, true);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") layers.at(-1)?.onDismiss();
});
// The modal owns its portal root; the select owns its trigger.
dismissable(modal, { owns: [portalRoot], onDismiss: closeModal });
dismissable(listbox, { owns: [trigger], onDismiss: closeSelect });
Which fix covers which click
Pick a strategy, open the list, run each test.
Portalled selectcontains()
if (!modal.contains(e.target)) closeModal()| List open, then… | contains() | + portal | Layer stack |
|---|---|---|---|
| Pick an option | – | – | – |
| Click the backdrop | – | – | – |
| Press Esc | – | – | – |
// Radix and Base UI track nested layers for their own primitives.
// A third-party portalled dropdown is invisible to them, so tell the Dialog:
<Dialog.Content
onPointerDownOutside={(e) => {
if (e.target.closest("[data-dropdown-portal]")) e.preventDefault();
}}
>
Why it matters, pros and cons
Why it matters
A portal puts the list outside the modal, so contains() calls it outside.
Pros
- Typed input survives a stray click.
- Click and Esc peel one layer.
- One stack covers any nesting depth.
Cons
- Radix and Base UI already handle their own layers.
- A layer left on the stack swallows later clicks.
- A portal check alone misses backdrop and Esc.