Skip to main content
xriptxr

Dialog

Info:Overlay
30

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

modal popup overlay lightbox confirm sheet

Live demo

live · @xtyle/astro

Dialog

Open a modal

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

Nothing to decide

A dialog with no footer slot renders no footer at all — no hairline, no padded band under the body. The close button is the only way out and says so at rest.

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.

Edit theme

A wide, editor-class host. The default lg caps at 48rem, which crushes a code editor or a multi-column form; size="xl" (64rem) and size="full" are the ready-made wide steps. For an exact width, a consumer styles ::part(dialog) from its own stylesheet.

export const theme = derive({
	bg: "#0f1115",
	accent: "#5b8cff",
	algorithm: "xtyle-default",
});

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.

While open, the host relocates to document.body — a modal <dialog> anchors to the nearest ancestor that establishes a containing block, so one declared inside a transformed or filtered panel would center on that panel rather than the viewport. The consequence is that it stops being a descendant of whatever declared it, precisely while it is visible: a bind:this container query and a framework-scoped selector both go dead on open and come back on close. Reach it by id (document.getElementById, :global(#that-id)), which survives the move.
An inherited property an app sets document-wide (cursor, font-family) reaches this component's internals only through its light-DOM host, so set it on the host rather than on the rendered dialog. A document-level rule cannot cross the shadow boundary, which is why an app that hides the OS cursor gets a cursorless overlay until the declaration moves onto the element itself.
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.
For a wide, editor-class host (a code editor, a multi-column form), reach for size="xl" (64rem) or size="full"; for an exact width or any other panel styling, target ::part(dialog) from the consumer's own stylesheet (xtyle-dialog::part(dialog) { width: min(1100px, 96vw); }).

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 DialogSize
sm md lg xl full
md
html svelte astro
Caps the dialog's width. xl (64rem) and full cover editor-class content; for an exact width, style ::part(dialog).
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 html: close-label string Close
html svelte astro
Accessible label for the built-in close button.
noCloseButton html: no-close-button boolean false
html svelte astro
Suppresses the built-in close button when the dialog supplies its own dismiss control.

Events

What the component emits, and what rides along on event.detail. The name in the first column is the one addEventListener takes.

EventDetailBindingsDescription
cancel
html svelte astro
Escape was pressed. Cancelable: prevent it to keep the dialog open.
close { reason }
html svelte astro
The dialog closed.

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.

MethodReturnsBindingsDescription
showModal() void
html svelte
Open the dialog as a modal: the top layer, a backdrop, focus moved inside, and the rest of the page inert. The open attribute reflects it, so a consumer can drive it either way.
close(reason?: DialogCloseReason) void
html svelte
Close the dialog and hand focus back to whatever opened it. Fires close whether it was dismissed, escaped, or closed from script, and reports which as detail.reasonescape, backdrop, dismiss, or api.

Appearance

Sizes

sm

.xtyle-dialog--sm

Compact: for short confirmations.

md

default
.xtyle-dialog

Default.

lg

.xtyle-dialog--lg

Roomy: for richer content.

xl

.xtyle-dialog--xl

Wide (64rem): editor-class content, multi-column forms.

full

.xtyle-dialog--full

Viewport width minus the responsive margin.

States

open

.xtyle-dialog::backdrop

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

close-hover

.xtyle-dialog__close:hover

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

close-active

.xtyle-dialog__close:active::after

Close button pressed; overlay paints the press tint.

close-focus-visible

.xtyle-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. 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

Success:::part(dialog) .xtyle-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

internal .xtyle-dialog::backdrop

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

--scrim

header

Success:::part(header) .xtyle-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

Success:::part(body) .xtyle-dialog__body

The scrolling content region between header and footer.

--space-5 --fg-1

footer

Success:::part(footer) .xtyle-dialog__footer

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

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

close

Success:::part(close) .xtyle-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 30/30 consumed tokens produced default register: 310 tokens

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

<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>