Skip to main content

Dialog

Info:Overlay html svelte astro Success: coverage 29/29

A centered modal built on the native `<dialog>` element: scrim, focus trap, and Esc-to-close come for free.

Live demo

live · @xoji/astro

Dialog

Open a modal

Each button opens a real <dialog> — scrim, focus trap, and Esc-to-close come from the platform.

Delete account?

This permanently removes your account and all of its data. This cannot be undone.

Sign in

A medium dialog comfortably holds a short form.

v3

Terms of service

The large dialog gives room for richer content — a custom header, a scrolling body, and a tidy action row.

By continuing you agree to the acceptable-use policy, the data-handling addendum, and the boring parts nobody reads. The body region scrolls on its own when content overflows, keeping the header and footer pinned.

Every surface here is derived from the same algorithm powering this page — the overlay panel, the hairlines, the scrim, and the focus ring are all materialized tokens, not bespoke styling.

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.

Pair the footer with Button for the action row; wire a ghost neutral Cancel to close() and the primary action to its handler.
Use heading for the common titled case; drop in a header slot when the title needs an icon, badge, or subtitle.
Set no-close-button when the dialog is a blocking confirm whose only exits are explicit footer actions.

Props

7 props, straight from the manifest.

PropTypeDefaultBindingsDescription
open boolean false
html svelte astro
Whether the modal is shown. Set it true to open; the wrappers bind it two-way.
size Size
sm md lg
md
html svelte astro
Caps the dialog's width.
heading string
html svelte astro
Title text rendered in the header and wired to the dialog via `aria-labelledby`.
label string
html svelte astro
Accessible name applied as `aria-label`: the way to name a dialog that supplies its own `header` slot instead of a `heading`.
labelledby string
html svelte astro
Id of an external element naming the dialog. Overrides the generated heading id.
closeLabel string Close
html svelte astro
Accessible label for the built-in close button.
noCloseButton boolean false
html svelte astro
Suppresses the built-in close button when the dialog supplies its own dismiss control.

Appearance

Sizes

sm

.xoji-dialog--sm

Compact: for short confirmations.

md

default
.xoji-dialog

Default.

lg

.xoji-dialog--lg

Roomy: for richer content.

States

open

.xoji-dialog::backdrop

Shown as a modal via showModal(), placed in the top layer with the scrim painted by ::backdrop.

close-hover

.xoji-dialog__close:hover

Pointer over the close button; overlay paints the hover tint and the icon brightens.

close-active

.xoji-dialog__close:active::after

Close button pressed; overlay paints the press tint.

close-focus-visible

.xoji-dialog__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

.xoji-dialog

The native <dialog> surface holding the modal; the elevated panel above the scrim.

--font-sans --text-body --leading-normal --fg-0 --surface-overlay --surface-overlay-border --border-thin --radius-lg --elevation-5 --space-5 --space-6

scrim

.xoji-dialog::backdrop

The native ::backdrop pseudo-element dimming the page behind the modal.

--scrim

header

.xoji-dialog__header

The top region carrying the title slot and the close button, separated by a hairline.

--space-3 --space-4 --space-5 --border-thin --line

body

.xoji-dialog__body

The scrolling content region between header and footer.

--space-5 --fg-1

footer

.xoji-dialog__footer

The bottom region for actions, right-aligned with a hairline above.

--space-2 --space-4 --space-5 --border-thin --line

close

.xoji-dialog__close

The default dismiss button in the header corner, drawn in currentColor.

--fg-2 --fg-0 --radius-sm --duration-fast --ease-standard --state-hover --state-press --border-normal --border-thick --ring

Tokens & coverage

What the component consumes, checked live against what the algorithm produces.

Success:fully covered 29/29 consumed tokens produced default register: 276 tokens

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

default
html svelte astro

The dialog body content.

header
html svelte astro

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
html svelte astro

Footer actions, right-aligned.

Accessibility

Built on the native <dialog> element, so role="dialog", aria-modal="true", the focus trap, focus restore on close, and Escape-to-dismiss all come from the browser.
showModal() places the dialog in the top layer and renders the ::backdrop scrim; a click on the backdrop closes it.
A heading (or explicit labelledby) wires the dialog to its title via aria-labelledby; a header-slot dialog names itself with label (an aria-label) instead, since the IDREF cannot cross into the slotted light DOM. The binding warns at runtime when none is present.
The built-in close button carries an aria-label (default "Close", overridable via closeLabel) and its glyph is aria-hidden.
Focus on the close button is shown with a token ring and a transparent outline that the forced-colors base rule promotes to a real system outline.
Closing (by Escape, the close button, a backdrop click, or close()) fires a close event and clears the open state.

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>