dialog
Tier 1Modal and drawer. Focus trap, Esc, top layer, backdrop — the browser's. Open/close — invoker commands (shimmed while support spreads).
Composition
The shadcn structure, in native vocabulary — header (title + description), body, footer are real elements, not components:
<button commandfor="confirm" command="show-modal">Open</button> <!-- trigger -->
<dialog id="confirm"> <!-- content -->
<button class="close" commandfor="confirm" command="close" aria-label="Close">✕</button>
<header> <!-- header -->
<h2>Are you sure?</h2> <!-- title -->
<p>This cannot be undone.</p> <!-- description -->
</header>
<p>Any body content.</p>
<footer> <!-- footer: right-aligned row -->
<form method="dialog">
<button>Cancel</button>
<button class="primary" value="ok">Confirm</button>
</form>
</footer>
</dialog>
Notes
Enter/exit animations use @starting-style +
transition-behavior: allow-discrete — the backdrop fades too.
dialog.returnValue carries the value of the submitting
button.
Bottom sheet + gestures
Below 40rem the drawer becomes a full-width bottom sheet (content
height, anchored to the bottom edge — the same pattern as shadcn's
mobile drawer). The optional drawer.js module (Tier 2) adds
a grab handle and swipe-to-dismiss: the sheet follows a downward drag,
resists upward, and closes past a third of its height or on a flick.
Without the module the markup still works — the handle only appears
when the behavior exists.
<script type="module" src="mica/drawer.js"></script>
On the sheet the handle is the dismiss affordance, so the corner
× is redundant there while it stays the only one on the
desktop side-sheet. Hide it below the breakpoint in your own CSS — this
page does exactly that:
@media (max-width: 40rem) {
dialog.drawer > button.close { display: none; }
}
Browser support
Invoker commands (commandfor/command) are
Baseline newly-available (Chrome 135+, Firefox 144+, Safari 26.2+).
Until they're widely available, include the shim — it installs nothing
where the browser handles commands natively, and its removal is a
one-line diff later:
<script type="module" src="mica/invoker.js"></script>