Skip to content

Dialog and Popover

Two new primitives changed the modal story in the last few years.

Native, modal, focus-trapping, escape-closing, backdrop-rendering, with a real browser-managed inert state for the rest of the page.

<button id="open-confirm">Delete account</button>
<dialog id="confirm">
<form method="dialog">
<p>Are you sure? This cannot be undone.</p>
<menu>
<button value="cancel">Cancel</button>
<button value="confirm">Yes, delete</button>
</menu>
</form>
</dialog>
<script>
const dialog = document.getElementById('confirm')
document.getElementById('open-confirm').onclick = () => dialog.showModal()
dialog.addEventListener('close', () => {
if (dialog.returnValue === 'confirm') {
// do the deletion
}
})
</script>

What you didn’t have to write:

  • A focus trap (the browser implements it).
  • An Escape handler (the browser implements it).
  • A backdrop (the browser renders it; style with ::backdrop).
  • Inerting the rest of the page (the browser does it).
  • A return value mechanism (form method="dialog" provides it).
  • Screen reader announcement of dialog open (the browser handles it).

The dialog is a real <dialog>. No portals. No aria-modal. No tabindex juggling. The browser knows it’s a dialog because it is a dialog.

<kit-dialog> is a thin wrapper that adds default styling, declarative open/close, and a built-in dialogModule that handles dialog.open and dialog.close commands by id — so any element with meta-command="dialog.open" meta-prop-target="my-dialog" opens it without writing JS.

For non-modal floating UI — menus, tooltips, dropdowns, command palettes — the popover attribute is what <dialog> is for modals.

<button popovertarget="settings-menu">Settings</button>
<div id="settings-menu" popover>
<button>Profile</button>
<button>Notifications</button>
<button>Sign out</button>
</div>

That works. No JavaScript. The browser handles open, close, light-dismiss (clicking outside), focus management, and the top-layer rendering that keeps the popover above clipped scroll containers.

popover="manual" for popovers that don’t auto-close. popovertargetaction="show|hide|toggle" for explicit control. :popover-open for state-based styling. popover-show / popover-hide events for behavior.

Most frontend “kits” ship a <Popover> or <Dropdown> that re-implements these behaviors in JavaScript, often poorly, often inconsistently across browsers. Kitsune doesn’t. We use the platform.

The Quill tutorial’s command palette (chapter 9) is a <div popover> with native search filtering. The confirm dialog (chapter 7) is <kit-dialog> wrapping <dialog>. The toast region uses aria-live="polite" and renders below the document so screen readers announce changes.

The pattern across all three: use the native primitive, decorate it through CSS and metadata, let the browser handle the hard parts.

<dialog> is in every modern browser as of 2022.

popover reached every modern browser by 2024.

If your audience includes browsers older than that, both have well-understood polyfills. For most apps in 2026, neither is necessary.

Next: Forms and Validation →