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. Three sizes (sm, md, lg) cap its width while it stays responsive on small screens.
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.
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 xoji-default register
(derive(xojiDefault, { 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.
<xoji-button variant="solid" tone="accent" onclick="document.getElementById('confirm').showModal()">
Delete account
</xoji-button>
<xoji-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">
<xoji-button variant="ghost" tone="neutral" onclick="document.getElementById('confirm').close()">
Cancel
</xoji-button>
<xoji-button variant="solid" tone="danger">Delete</xoji-button>
</div>
</xoji-dialog>
<script lang="ts">
import { Button, Dialog } from "@xoji/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 "@xoji/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("xoji-dialog");
document.getElementById("open-confirm")?.addEventListener("click", () => dialog?.showModal());
document.getElementById("cancel-confirm")?.addEventListener("click", () => dialog?.close());
</script>