Dialog
A modal overlay for a task the user must complete or dismiss before returning to the page. Built on the native <dialog> element.
Playground
<script lang="ts">
import { Dialog } from '@urbicon-ui/blocks';
let open = $state(false);
</script>
<Dialog
bind:open
title="Delete project"
>
<p class="text-text-secondary text-sm">
{#if values.title}
This is the dialog body. Structured layout with header, body, and footer.
{:else}
This is a content-agnostic overlay. Adjust the controls to see how size, placement, and
dismissal behavior change.
{/if}
</p>
{#if values.title}
{#snippet footer()}
<div class="flex justify-end gap-2">
<Button variant="ghost" onclick={() => (open = false)}>Cancel</Button>
<Button onclick={() => (open = false)}>Confirm</Button>
</div>
{/snippet}
{:else}
<div class="mt-4 flex justify-end gap-2">
<Button variant="ghost" onclick={() => (open = false)}>Cancel</Button>
<Button onclick={() => (open = false)}>Confirm</Button>
</div>
{/if}
</Dialog>01 Examples
Confirmation (content-only)
<Dialog bind:open>
<p class="text-text-primary text-sm font-medium">Delete this item?</p>
<p class="text-text-tertiary mt-1 text-sm">This action cannot be undone.</p>
<div class="flex justify-end gap-2 mt-4">
<Button variant="ghost" onclick={() => open = false}>Cancel</Button>
<Button intent="danger" onclick={() => open = false}>Delete</Button>
</div>
</Dialog>Form Dialog
<Dialog bind:open title="Create Account" size="md">
<form>
<Input label="Name" bind:value={name} />
<Input label="Email" bind:value={email} />
</form>
{#snippet footer()}
<Button variant="ghost" onclick={() => open = false}>Cancel</Button>
<Button type="submit">Create</Button>
{/snippet}
</Dialog>Top Placement (command palette)
<Dialog bind:open placement="top" size="md">
<input type="text" placeholder="Search..." />
...results...
</Dialog>Scrollable Content
<Dialog bind:open title="Terms of Service" size="lg">
<div class="space-y-4"><p>Long content here...</p></div>
{#snippet footer()}
<Button variant="ghost" onclick={() => open = false}>Decline</Button>
<Button onclick={() => open = false}>Accept</Button>
{/snippet}
</Dialog>02 Customization
Neon terminal
<Dialog
bind:open
slotClasses={{
panel: 'border-green-500/30 bg-black text-green-400 shadow-[0_0_30px_rgba(34,197,94,0.15)]',
content: 'font-mono text-xs'
}}
>
...
</Dialog>This is one of five ways to restyle a block. See Customization for class, slotClasses, unstyled, preset and provider-level overrides.
03 Accessibility
Native Dialog
Built on the native <dialog> element opened
with showModal(), so the page behind it goes inert. A title becomes the dialog's accessible name through aria-labelledby.
Focus Trap
On open, focus moves to the first interactive element inside the panel. While open, focus is trapped: Tab cycles the interactive elements and never leaves. On close, focus returns to the element that opened the dialog.
Dismissal
Escape closes the dialog (closeOnEscape), and so does a
click on the backdrop (closeOnBackdropClick). Both
are on by default. onClose fires on any dismissal, so reset
your state there.
Scroll Lock
While open, body scroll is locked. Long dialog content scrolls within the panel itself.
04 API Reference
20 propsProp | Type | Default | Description | |
|---|---|---|---|---|
children required | Snippet | — | Dialog body content. When title is omitted the content fills the entire panel. | |
class | string | — | Additional CSS classes applied to the dialog panel. | |
closeOnBackdropClick | boolean | true | Whether clicking the backdrop dismisses the dialog.
Turning this off together with closeOnEscape and hideCloseButton is a
legitimate pattern for a forced choice (terms to accept, data loss to
confirm, an expired session) — but only when the dialog itself renders the
action that resolves it. Without one there is no way out, and DEV warns. | |
closeOnEscape | boolean | true | Whether pressing Escape dismisses the dialog.
The ARIA APG recommends Escape and does not forbid disabling it; the same
condition as closeOnBackdropClick applies — with every exit closed,
the dialog must carry its own action.
Escape is dismissed one layer at a time: a control INSIDE the dialog that
handles Escape itself — an open Select/Combobox/Menu panel, a
clearable Input with text in it — consumes the key, and the dialog stays
up. The second Escape closes the dialog. This is about controls in the
content; it does not apply to a consumer onkeydown on the Dialog itself,
which cannot veto the dismiss (see utils/compose-handlers.ts) — use this
prop for that. | |
draggable | boolean | false | Let the user reposition the dialog by dragging its header. Off by default.
The structured header (rendered when title is set) becomes the drag
handle; the close button and any header controls stay clickable, and the
offset resets each time the dialog reopens. Most useful for a dialog the
user may want to shove aside to see the content behind it. Requires a
title — without a header there is nothing to grab. Overrides the native
HTML draggable attribute, which has no meaningful use on a dialog. | |
footer | Snippet | — | Action buttons rendered in a footer bar. Only rendered when provided. | |
hideCloseButton | boolean | false | Hides the built-in close button. Only takes effect when title is
also set — the close button only renders in the structured (titled)
header. Without a title, no close button exists to hide.
The last of the three exits; see closeOnBackdropClick for what
closing all of them requires. | |
icon | Snippet | — | Icon rendered before the title in the header bar. Takes its colour from
intent, so a bare icon component is enough — no wrapper needed. Marked
aria-hidden: it repeats the intent the title already carries in words.
Requires title (no header renders without one). | |
intent | DialogVariants['intent'] | 'neutral' | Semantic purpose marker (e.g. danger for destructive actions). Tints the
header markers — the title and, when given, icon. The panel surface stays
neutral by design: this is a container for arbitrary content, and a tinted
surface would hijack the contrast ratios of the form, table or code block
inside it (the Toast makes the same split). The value is additionally
exposed on the panel as data-intent="…" for presets and CSS overrides. | |
onClose | () => void | — | Fires when the dialog is dismissed via Escape, backdrop click, or close button. | |
open | boolean | — | Controls whether the dialog is visible. Supports bind:open. | |
placement | DialogVariants['placement'] | 'center' | Vertical placement within the viewport. Use 'top' for command-palette style positioning. | |
preset | string | — | Apply a named preset registered via <BlocksProvider presets={{ Dialog: {...} }}>.
Prefer this over class overrides when the requested look falls outside the
semantic intent palette — presets keep hover/active/dark-mode logic coherent
and make the custom look reusable across the project. | |
size | DialogVariants['size'] | 'sm' | Controls the maximum width of the dialog panel. | |
slotClasses | Partial<Record<DialogSlots, string>> | — | Per-slot class overrides merged with variant styles. | |
title | string | — | Heading displayed in a header bar. Enables the structured header/body/footer layout. | |
transitionDuration | number | — | Override the enter/exit animation duration in milliseconds.
Defaults to the overlay token --blocks-overlay-enter-duration /
--blocks-overlay-exit-duration (200ms / 180ms). Set globally via
the CSS custom properties or per-instance via this prop. Respects
prefers-reduced-motion. | |
transitionEasing | (t: number) => number | — | Override the enter/exit easing function. Defaults to the overlay token easing (quintOut). | |
unstyled | boolean | — | Strip all default styles. Combine with slotClasses for fully custom appearance. | |
...HTMLDialogAttributes inherited | HTMLAttributes | — | HTML attributes (excluding: 'children' | 'open' | 'draggable') |
05 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
DialogProps | interface | props | 0 | — | |
DialogSize | type | helper | 0 | Maximum-width preset for the dialog panel. Mirrors the size variant
exposed via DialogProps.size. | |
DialogPlacement | type | helper | 0 | Vertical placement of the dialog within the viewport. | |
DialogIntent | type | helper | 0 | Semantic intent of the dialog. Tints the header markers (title, icon) and is
mirrored on the panel as data-intent. | |
DialogVariants | type | variant | 0 | — | |
DialogSlots | type | variant | 0 | Slot names derived from the tv() config above — single source of truth for slotClasses. |
06 Installation
Import
import { Dialog } from '@urbicon-ui/blocks';