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 |
|---|---|---|---|---|
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, with their selectors.
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
--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-semibold
Slots
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="accent" 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 lang="ts">
import { Button, Dialog } from "@xtyle/svelte";
let open = $state(false);
</script>
<Button variant="solid" tone="accent" 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="accent" 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>