Dialog
Overlay dialog with optional structured layout (title/footer/intent), focus trapping, and keyboard management. Built on native <dialog>.
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
Slot Class Overrides
<Dialog
bind:open
title="Quick Settings"
slotClasses={{
panel: 'rounded-2xl',
header: 'bg-surface-subtle',
body: 'bg-surface-base',
footer: 'bg-surface-subtle'
}}
>
...
</Dialog>Fully Custom (unstyled)
<Dialog
unstyled
bind:open
slotClasses={{
dialog: 'fixed inset-0 z-[var(--z-modal)] flex items-center justify-center p-4',
backdrop: 'fixed inset-0 bg-black/80',
panel: 'relative w-full max-w-xs border border-green-500/30 bg-black text-green-400',
content: 'p-5 font-mono text-xs'
}}
>
...
</Dialog>A dialog chrome shared across the app belongs in presets.Dialog on BlocksProvider — and because ConfirmDialog forwards its
styling props to the inner Dialog, the same preset covers both components. See Customization.
03 Accessibility
Native Dialog
Built on <dialog> with showModal() for native inertness and stacking
context. Screen readers announce it automatically via aria-modal="true". When a title is set, it is linked via aria-labelledby.
Focus Trap
When open, focus is trapped inside the dialog. Tab cycles through interactive elements. On close, focus returns to the element that opened the dialog.
Keyboard
Escape closes the dialog (configurable via closeOnEscape).
Scroll Lock
While open, body scroll is locked. Long dialog content scrolls within the panel itself.
04 API Reference
Prop | 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';