Overlays
Dialog
A promise-based imperative dialog system for confirmations and one-field prompts. Dialogs stack on top of each other, each with its own focus trap, backdrop, and ESC handler. A form goes in a Drawer, which sizes itself to its fields. A one-field prompt may sit in a Modal. In a console a row is created and edited in a Drawer.
Setup
Mount <DialogContainer /> once near the root of your layout. It reads from the module-level
state and renders the dialog stack.
<script lang="ts">
import { DialogContainer } from '@lyeve-labs/ui-kit';
let { children } = $props();
</script>
{@render children()}
<DialogContainer />Confirm dialog
The confirm() shortcut is the simplest path: open a small dialog, get back a boolean.
Its third argument names the buttons, and the ConfirmDialog page shows it live.
<script lang="ts">
import { confirm } from '@lyeve-labs/ui-kit';
async function handleDelete(id: string) {
const ok = await confirm('Delete this item?', 'This action cannot be undone.', {
confirmLabel: 'Delete',
detail: id,
});
if (!ok) return;
await fetch(`/api/items/${id}`, { method: 'DELETE' });
}
</script>Custom body and footer
Use openDialog() for full control. Pass a body and a footer snippet, then call closeDialog() with a payload or dismissDialog() to cancel. A snippet declared at the top of the component can be handed
to it by name.
<script lang="ts">
import { Button, Input, closeDialog, dismissDialog, openDialog } from '@lyeve-labs/ui-kit';
let name = $state('');
async function openCreate() {
// Resolves with the payload passed to closeDialog(). Dismissing the
// dialog rejects it instead: see Error handling below.
const result = await openDialog<string>({
title: 'Create project',
body: createBody,
footer: createFooter,
});
console.log('Created:', result);
}
</script>
{#snippet createBody()}
<Input label="Project name" bind:value={name} />
{/snippet}
{#snippet createFooter()}
<Button variant="secondary" onclick={() => dismissDialog()}>Cancel</Button>
<Button onclick={() => closeDialog(name)}>Create</Button>
{/snippet}
<Button onclick={openCreate}>New project</Button>Blocking a close
Pass onClose to block dismissal when there is unsaved work. Return false to
keep the dialog open.
const result = await openDialog({
title: 'Rename view',
persistent: false,
onClose: () => {
if (nameChanged) return false; // stays open
},
body: renameSnippet,
footer: actionsSnippet,
});Persistent dialogs
Set persistent: true to block ESC and backdrop-click dismissal. Only an explicit dismissDialog(id) (or confirming with closeDialog()) will close it.
<script lang="ts">
import { Button, closeDialog, dismissDialog, openDialog } from '@lyeve-labs/ui-kit';
async function askTerms() {
const result = await openDialog<string>({
id: 'terms',
title: 'Required action',
persistent: true,
body: termsBody,
footer: termsFooter,
});
console.log('Terms:', result);
}
</script>
{#snippet termsBody()}
<p>Accept the updated terms to keep using the workspace.</p>
{/snippet}
{#snippet termsFooter()}
<Button variant="ghost" onclick={() => dismissDialog('terms')}>Decline</Button>
<Button onclick={() => closeDialog('accepted', 'terms')}>Accept</Button>
{/snippet}
<Button onclick={askTerms}>Review terms</Button>Stack and lifecycle
Dialogs stack last-on-top. openDialog() pushes, closeDialog() resolves, dismissDialog() rejects. dismissDialog() without an id does not
dismiss persistent dialogs.
import {
openDialog,
closeDialog,
dismissDialog,
dismissAllDialogs,
getDialogStack,
} from '@lyeve-labs/ui-kit';
const a = openDialog({ title: 'First' });
const b = openDialog({ title: 'Second' });
getDialogStack(); // [{ depth: 0, ... }, { depth: 1, ... }]
dismissDialog(); // dismisses "Second"
closeDialog('done'); // resolves "First" with "done"
dismissAllDialogs(); // rejects every open dialogSizes
A dialog takes a rung of the shared overlay ladder, the same one Modal and Drawer use. It has no auto size. openDialog() opens at md unless the call names a
rung, and confirm() names sm. That suits the confirmation and the
one-field prompt the stack is for. A form goes in a Drawer, which measures its fields and takes
the rung they need. A one-field prompt may sit in a Modal.
sm: 448px, the default forconfirm()md: 576px, the default foropenDialog()lg: 704pxxl: 896pxfull: 1088px, for a dialog holding a table
Under a touch screen
The close cross takes a 44px target, as the crosses on Modal and Drawer do. It is drawn the same size as under a mouse.
Error handling
Dismissed dialogs (backdrop, ESC, or dismissDialog()) reject the promise with a DOMException named AbortError. Catch it explicitly or use .catch():
try {
const result = await openDialog({ title: 'Edit' });
await save(result);
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') {
// the reader canceled, so there is nothing to do
return;
}
throw err;
}API
DialogContainer
No props. Mount once at the app root. It reads the reactive dialog stack and renders each entry as
a <Dialog>.
Dialog
The frame DialogContainer draws for each entry in the stack: the focus trap, Escape
and the backdrop, the motion and the stacking. A page opens it through openDialog() and
rarely renders it directly.
| Prop | Type | Default | Description |
|---|---|---|---|
entry required | DialogEntry<any> | no default | The stack entry it draws: its options, its place in the stack and how it answers. |
children | Snippet | no default | The body, when the entry carries none and is not a confirm. |
openDialog
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | no default | Custom id. Auto-generated dialog-N if omitted. |
title | string | Snippet | no default | Dialog heading. |
body | Snippet | no default | Main content. |
footer | Snippet | no default | Footer actions. No footer if omitted and non-persistent. |
size | DialogSize | 'md' | A rung of the overlay ladder: 448, 576, 704, 896, 1088px from sm to full. There is no auto, so a form goes in a Drawer. A one-field prompt may sit in a Modal. |
persistent | boolean | false | Block ESC and backdrop dismissal. |
onClose | () => boolean | void | no default | Return false to prevent closing. |
Imperative functions
| Prop | Type | Default | Description |
|---|---|---|---|
confirm(title, message?, options?) | Promise<boolean> | no default | Shortcut: opens a sm confirm dialog. Resolves true if confirmed, false if canceled. options names the buttons and adds a detail line. |
openDialog<T>(options) | Promise<T> | no default | Opens a dialog. Resolves on closeDialog(), rejects on dismissDialog(). |
closeDialog(value, id?) | void | no default | Resolves the dialog promise. Omit id to close the topmost. |
dismissDialog(id?) | void | no default | Rejects the dialog promise with AbortError. Ignores persistent dialogs unless id is explicit. |
dismissAllDialogs() | void | no default | Rejects every open dialog and resets body scroll lock. |
setDialogMeta(id, meta) | void | no default | Merge metadata into a dialog entry. Keys: confirmTitle, confirmMessage, confirmDetail, confirmLabel, cancelLabel. |
getDialogStack() | readonly DialogEntry[] | no default | Snapshot of the current dialog stack. |
Types
| Prop | Type | Default | Description |
|---|---|---|---|
DialogSize | 'sm' | 'md' | 'lg' | 'xl' | 'full' | no default | Max-width preset. |
DialogOptions<T> | interface | no default | id, title, body, footer, size, persistent, onClose. |
DialogEntry<T> | interface | no default | id, options, depth, meta. Depth 0 is the bottom dialog. |