Close only the top layer

An outside click closes the portalled popover, not the modal behind it.

Emil Kowalski@emilkowalski
When 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.
Jul 10, 2026View on X ↗

From emil-design-eng.

One modal, two dismissal rules

Type a client, open Terms, then click the backdrop.

Each layer checks DOMBad
lost 0
Top layer dismissesGood
lost 0
// 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.

Dismissal strategy
Portalled selectcontains()
if (!modal.contains(e.target)) closeModal()
Does the modal survive each action?
List open, then…contains()+ portalLayer 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.