Dialog
A centered modal built on the native <dialog> element: scrim, focus trap, and Esc-to-close come for free.
Live demo
Dialog is a centered modal that wraps the platform <dialog> element, so the native modal machinery (the top-layer scrim, the focus trap, focus restore on close, Escape to dismiss, and the role/aria-modal semantics) all comes from the browser rather than re-implemented JavaScript. Open and close it imperatively with showModal() / close() (or the reactive open prop in the framework wrappers).
It lays out a header, a scrolling body, and a footer via named slots, and ships a close button by default. The header is wired to the dialog with aria-labelledby whenever a heading (or explicit labelledby) is supplied; a dialog that brings its own header slot instead names itself with label, since aria-labelledby cannot reach a slotted title across the shadow boundary. Five sizes (sm, md, lg, xl, full) cap its width while it stays responsive on small screens; xl (64rem) and full cover editor-class content, and the inner panel is exposed as ::part(dialog) so a consumer can size or style it to an exact width directly.
When to use
How this component composes with the rest of the set.
Props
7 props, straight from the manifest.
| Prop | Type | Default | Bindings | Description |
|---|---|---|---|---|
xl (64rem) and full cover editor-class content; for an exact width, style ::part(dialog). | ||||
aria-labelledby. | ||||
aria-label: the way to name a dialog that supplies its own header slot instead of a heading. | ||||
Events
What the component emits, and what rides along on event.detail. The name in the
first column is the one addEventListener takes.
| Event | Detail | Bindings | Description |
|---|---|---|---|
Methods
What you can call on the element itself — the half of the surface a prop or an event cannot express. A control driven by a gesture, or one whose state the platform owns, opens here. Astro renders on the server and hands back no instance, so no method is reachable from it.
| Method | Returns | Bindings | Description |
|---|---|---|---|
open attribute reflects it, so a consumer can drive it either way. | |||
close whether it was dismissed, escaped, or closed from script, and reports which as detail.reason — escape, backdrop, dismiss, or api. |
Appearance
Sizes
sm
Compact: for short confirmations.
md
Default.
lg
Roomy: for richer content.
xl
Wide (64rem): editor-class content, multi-column forms.
full
Viewport width minus the responsive margin.
States
open
Shown as a modal via showModal(), placed in the top layer with the scrim painted by ::backdrop.
close-hover
Pointer over the close button; overlay paints the hover tint and the icon brightens.
close-active
Close button pressed; overlay paints the press tint.
close-focus-visible
Keyboard focus on the close button; a token ring plus the transparent outline promoted in forced-colors mode.
Anatomy
The named parts that make up the component. A ::part() handle is reachable from your own stylesheet; the class beside it is the component's internal selector, which a shadow boundary keeps to itself.
dialog
The native <dialog> surface holding the modal; the elevated panel above the scrim.
scrim
The native ::backdrop pseudo-element dimming the page behind the modal.
header
The top region carrying the title slot and the close button, separated by a hairline.
body
The scrolling content region between header and footer.
footer
The bottom region for actions, right-aligned with a hairline above.
close
The default dismiss button in the header corner, drawn in currentColor.
Tokens & coverage
What the component consumes, checked live against what the algorithm produces.
Live coverage check against the xtyle-default register
(derive(xtyleDefault, { anchors }) →
coverComponent(manifest, register)). Every token this component
consumes must be a key the algorithm produces.
--border-normal--border-thick--border-thin--duration-fast--ease-standard--elevation-5--fg-0--fg-1--fg-2--fg-3--font-sans--leading-normal--leading-tight--line--radius-lg--radius-sm--ring--scrim--space-2--space-3--space-4--space-5--space-6--state-hover--state-press--surface-overlay--surface-overlay-border--text-body--text-lg--weight-semiboldSlots
The dialog body content.
Custom header content, replacing the generated title. Falls back to the heading text; pair it with label to give the dialog an accessible name.
Footer actions, right-aligned.
Accessibility
Code
Confirmation dialog
A titled modal with a body message and a Cancel / Delete action row in the footer.
<xtyle-button variant="solid" tone="danger" onclick="document.getElementById('confirm').showModal()">
Delete account
</xtyle-button>
<xtyle-dialog id="confirm" heading="Delete account?">
<p>This permanently removes your account and all of its data. This cannot be undone.</p>
<div slot="footer">
<xtyle-button variant="ghost" tone="neutral" onclick="document.getElementById('confirm').close()">
Cancel
</xtyle-button>
<xtyle-button variant="solid" tone="danger">Delete</xtyle-button>
</div>
</xtyle-dialog>
<script>
document.getElementById("confirm").addEventListener("close", (event) => {
if (event.detail.reason !== "api") console.log("dismissed without deciding");
});
</script><script lang="ts">
import { Button, Dialog } from "@xtyle/svelte";
let open = $state(false);
</script>
<Button variant="solid" tone="danger" onclick={() => (open = true)}>Delete account</Button>
<Dialog bind:open heading="Delete account?" size="sm">
<p>This permanently removes your account and all of its data. This cannot be undone.</p>
{#snippet footer()}
<Button variant="ghost" tone="neutral" onclick={() => (open = false)}>Cancel</Button>
<Button variant="solid" tone="danger" onclick={() => (open = false)}>Delete</Button>
{/snippet}
</Dialog>---
import { Button, Dialog } from "@xtyle/astro";
---
<Button variant="solid" tone="danger" id="open-confirm">Delete account</Button>
<Dialog heading="Delete account?">
<p>This permanently removes your account and all of its data. This cannot be undone.</p>
<div slot="footer">
<Button variant="ghost" tone="neutral" id="cancel-confirm">Cancel</Button>
<Button variant="solid" tone="danger">Delete</Button>
</div>
</Dialog>
<script>
const dialog = document.querySelector("xtyle-dialog");
document.getElementById("open-confirm")?.addEventListener("click", () => dialog?.showModal());
document.getElementById("cancel-confirm")?.addEventListener("click", () => dialog?.close());
</script>