Component
Dialog
<DomDialog>A modal built on the native HTML <dialog> element — top-layer rendering, native stacking, scroll lock, and backdrop styling without z-index hacks.
Playground
Try every prop live
Dialog playground
Edit any prop on the right — open the dialog with the trigger to preview title, description, and footer actions. Source shows the SFC you would write; Data shows the live props object.
<script setup>
import { reactive } from 'vue';
import { DomDialog, DomButton } from '@getdom/studio/vue';
const data = reactive({
modelValue: false,
static: false,
backdrop: true,
title: 'Dialog title',
description: 'Supporting text shown under the heading.',
});
defineExpose({ data });
</script>
<template>
<DomDialog
v-bind="data"
@update:modelValue="data.modelValue = $event"
>
<template #trigger>
<DomButton variant="primary">Open dialog</DomButton>
</template>
<p class="text-sm text-muted-fg">Dialog body content.</p>
<template #footer>
<DomButton variant="secondary" data-close>Cancel</DomButton>
<DomButton data-close>Confirm</DomButton>
</template>
</DomDialog>
</template>
Demo
Delete confirmation
A destructive action pattern — title, description, body copy, and footer buttons bound with v-model.
<script setup>
import { ref } from 'vue';
import { DomDialog, DomButton } from '@getdom/studio/vue';
const open = ref(false);
</script>
<template>
<DomButton variant="danger" @click="open = true">Delete project</DomButton>
<DomDialog
v-model="open"
title="Delete project?"
description="This will permanently delete the project and all of its data."
>
<p class="text-sm text-muted-fg">There is no undo. Type the project name to confirm.</p>
<input
class="mt-3 h-10 w-full rounded-full border border-border bg-background px-4 text-sm"
placeholder="my-project"
/>
<template #footer>
<DomButton variant="secondary" @click="open = false">Cancel</DomButton>
<DomButton variant="danger" @click="open = false">Delete</DomButton>
</template>
</DomDialog>
</template>
Demo
Scrollable terms
A long-form agreement pattern that keeps the dialog footer visible while the terms scroll inside the dialog body.
<script setup>
import { nextTick, ref, watch } from 'vue';
import { DomDialog, DomButton } from '@getdom/studio/vue';
const open = ref(false);
const canAccept = ref(false);
const terms = ref(null);
const sections = [
{
title: 'Account access',
body: 'You are responsible for keeping your account details accurate and for protecting access to your workspace. Notify the service team if you believe your account has been used without permission.',
},
{
title: 'Acceptable use',
body: 'Do not use the service to interfere with other customers, attempt to bypass security controls, scrape private data, or upload content that you do not have permission to process.',
},
{
title: 'Customer content',
body: 'You retain ownership of your content. The service may process, store, and transmit that content only as needed to provide the features you choose to use.',
},
{
title: 'Service changes',
body: 'Features may change as the product evolves. When a material change affects your rights or obligations, updated terms will be made available before they take effect.',
},
{
title: 'Billing',
body: 'Paid plans renew automatically unless cancelled before the renewal date. Taxes, usage charges, and subscription changes may be reflected on future invoices.',
},
{
title: 'Privacy',
body: 'Personal data is handled according to the privacy notice. Administrative users should make sure their teams understand what information is submitted to the service.',
},
{
title: 'Availability',
body: 'The service is designed for reliable access, but planned maintenance, emergency repairs, third-party outages, and network conditions may affect availability from time to time.',
},
{
title: 'Termination',
body: 'Either party may end use of the service according to the plan terms. After termination, access may be limited and retained data may be deleted after the applicable retention period.',
},
];
function updateScrollState() {
const el = terms.value;
if (!el) return;
canAccept.value = el.scrollTop + el.clientHeight >= el.scrollHeight - 8;
}
watch(open, async (isOpen) => {
if (!isOpen) return;
canAccept.value = false;
await nextTick();
if (terms.value) terms.value.scrollTop = 0;
updateScrollState();
});
function acceptTerms() {
if (!canAccept.value) return;
open.value = false;
}
</script>
<template>
<DomButton variant="primary" @click="open = true">Review terms</DomButton>
<DomDialog
v-model="open"
title="Terms and conditions"
description="Read the full agreement before accepting."
>
<div
ref="terms"
class="max-h-[min(52vh,22rem)] overflow-y-auto rounded-xl border border-border bg-secondary/30 p-4 pr-3 text-sm leading-6 text-muted-fg"
@scroll="updateScrollState"
>
<div class="space-y-5">
<section v-for="section in sections" :key="section.title" class="space-y-1">
<h3 class="text-sm font-semibold text-fg">{{ section.title }}</h3>
<p>{{ section.body }}</p>
</section>
<p class="border-t border-border pt-5 text-fg">
By accepting, you confirm that you have reviewed the terms above and are authorised to agree on behalf of your workspace.
</p>
</div>
</div>
<template #footer>
<p class="mr-auto text-xs text-muted-fg">
{{ canAccept ? 'Ready to accept.' : 'Scroll to the end to continue.' }}
</p>
<DomButton variant="secondary" @click="open = false">Cancel</DomButton>
<DomButton :disabled="!canAccept" @click="acceptTerms">Accept</DomButton>
</template>
</DomDialog>
</template>
Demo
Programmatic control
Omit the trigger slot — open, close, or toggle via a template ref, or drive state with v-model.
<script setup>
import { ref } from 'vue';
import { DomDialog, DomButton } from '@getdom/studio/vue';
const dialogRef = ref(null);
</script>
<template>
<div class="flex flex-wrap items-center justify-center gap-2">
<DomButton @click="dialogRef?.open()">Open</DomButton>
<DomButton variant="secondary" @click="dialogRef?.close()">Close</DomButton>
<DomButton variant="ghost" @click="dialogRef?.toggle()">Toggle</DomButton>
</div>
<DomDialog
ref="dialogRef"
title="Programmatic control"
description="No trigger slot — open from script via the component ref."
>
<p class="text-sm text-muted-fg">
Call <code class="text-fg">open()</code>, <code class="text-fg">close()</code>, or
<code class="text-fg">toggle()</code> on the ref, or bind <code class="text-fg">v-model</code>.
</p>
<template #footer>
<DomButton variant="secondary" data-close>Done</DomButton>
</template>
</DomDialog>
</template>
Demo
Programmatic dialogs
Mount one stack and call useDialogs() from script to await confirmations, component forms, or schema-driven forms.
No dialog result yet.
<script setup>
import { ref } from 'vue';
import { DomButton, DomDialogStack, useDialogs } from '@getdom/studio/vue';
import InviteDialogForm from './InviteDialogForm.vue';
const {
dialogStack,
confirmDialog,
dialogForm,
resolveDialog,
dismissDialog,
} = useDialogs();
const lastResult = ref('No dialog result yet.');
const schemaFormFields = [
{
component: 'DomTextInput',
props: {
name: 'name',
label: 'Name',
required: true,
placeholder: 'Maya Patel',
},
},
{
component: 'DomEmailInput',
props: {
name: 'email',
label: 'Email',
required: true,
placeholder: 'maya@example.com',
},
},
];
async function confirmArchive() {
const confirmed = await confirmDialog({
title: 'Archive project?',
description: 'The project will move out of the active workspace.',
message: 'You can restore archived projects from workspace settings.',
confirmText: 'Archive',
tone: 'danger',
});
lastResult.value = confirmed ? 'Archive confirmed.' : 'Archive cancelled.';
}
async function openInviteForm() {
const result = await dialogForm(InviteDialogForm, {
defaultEmail: 'maya@example.com',
}, {
title: 'Invite teammate',
description: 'Resolve the dialog with data from a custom form component.',
});
lastResult.value = result?.email ? `Invite queued for ${result.email}.` : 'Invite cancelled.';
}
async function openSchemaForm() {
const result = await dialogForm(schemaFormFields, {
initialValues: {
name: 'Maya Patel',
email: 'maya@example.com',
},
title: 'Schema-defined invite',
description: 'The dialog renders an DomForm from a programmatic children schema.',
confirmText: 'Create invite',
});
lastResult.value = result?.email
? `Schema form resolved for ${result.name} (${result.email}).`
: 'Schema form cancelled.';
}
</script>
<template>
<div class="flex flex-col items-center gap-4">
<div class="flex flex-wrap items-center justify-center gap-2">
<DomButton variant="danger" @click="confirmArchive">Confirm action</DomButton>
<DomButton variant="secondary" @click="openInviteForm">Open form</DomButton>
<DomButton variant="secondary" @click="openSchemaForm">Open schema form</DomButton>
</div>
<p class="text-sm text-muted-fg">{{ lastResult }}</p>
</div>
<DomDialogStack
:dialogs="dialogStack"
@resolve="resolveDialog($event.id, $event.value)"
@dismiss="dismissDialog($event.id, $event.value)"
/>
</template>
Demo
Nested dialogs
Nest a second dialog inside the first — native top-layer stacking means no z-index gymnastics. Esc dismisses the topmost dialog; the one behind stays open.
<script setup>
import { ref } from 'vue';
import { DomDialog, DomButton, DomTextInput } from '@getdom/studio/vue';
const outer = ref(false);
const confirm = ref(false);
const projectName = ref('');
</script>
<template>
<DomButton variant="danger" @click="outer = true">Delete project…</DomButton>
<DomDialog
v-model="outer"
title="Delete project?"
description="This action cannot be undone."
>
<p class="text-sm text-muted-fg">All issues, comments, and uploads will be removed.</p>
<template #footer>
<DomButton variant="secondary" @click="outer = false">Cancel</DomButton>
<DomButton variant="danger" @click="confirm = true">Continue</DomButton>
</template>
<DomDialog
v-model="confirm"
title="Final confirmation"
description="Type the project name to confirm deletion."
>
<DomTextInput v-model="projectName" label="Project name" placeholder="my-project" />
<template #footer>
<DomButton variant="secondary" @click="confirm = false">Back</DomButton>
<DomButton
variant="danger"
:disabled="projectName !== 'my-project'"
@click="confirm = false; outer = false; projectName = ''"
>
Delete forever
</DomButton>
</template>
</DomDialog>
</DomDialog>
</template>
Demo
Custom dialogs
Full inner control
<script setup>
import { ref } from 'vue';
import { DomDialog, DomButton } from '@getdom/studio/vue';
const open = ref(false);
</script>
<template>
<DomButton variant="danger" @click="open = true">Delete project</DomButton>
<DomDialog
v-model="open"
static
title="Delete project?"
description="This will permanently delete the project and all of its data."
>
<p class="text-sm text-muted-fg">There is no undo. Type the project name to confirm.</p>
<input
class="mt-3 h-10 w-full rounded-full border border-border bg-background px-4 text-sm"
placeholder="my-project"
/>
<template #footer>
<DomButton variant="secondary" data-close @click="open = false">Cancel</DomButton>
<DomButton variant="danger" data-close @click="open = false">Delete</DomButton>
</template>
</DomDialog>
</template>
Usage
Plain HTML
Use the headless custom element when you want the same native dialog behaviour in plain HTML or another framework. The headless page includes copyable dialog examples with trigger, close, static, and programmatic controls.
View headless dialogWhy <dialog>
Built on the platform
Top layer
showModal() promotes the dialog above every stacking context — no overflow, transform or z-index can clip it.
Native stacking
Open as many dialogs as you need; the browser stacks them and Esc dismisses the topmost.
Scroll lock
Body scroll is locked automatically while a modal is open.
::backdrop
A pseudo-element you can style with CSS — blur, fade, animate, all native.
Reference
Props
Control props
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
modelValue | boolean | boolean | false | Whether the dialog is open. |
title | string | string | '' | Heading rendered inside the dialog card. |
description | string | string | '' | Supporting copy under the heading. |
static | boolean | boolean | false | Modal cannot be dismissed by backdrop click or Esc — use footer actions with data-close or v-model. |
backdrop | boolean | boolean | true | Show the dimmed, blurred page backdrop behind the dialog. |
footer | boolean | boolean | true | Show the footer region and default Close action when no footer slot is provided. |
Auto-generated from Dialog.props and inline _edit hints.
Slots
| Name | Scope | Description |
|---|---|---|
| #trigger | — | Optional. Element that opens the dialog when clicked — omit when using v-model, a template ref, or commandfor on an external button. |
| #(default) | — | Dialog body — rendered inside the card below the title and description. |
| #footer | — | Action buttons. Add `data-close` on a control to dismiss the dialog. |
Events
| Name | Payload | Description |
|---|---|---|
| @update:modelValue | ( | Emitted when the dialog opens or closes. |
| @close | — | Emitted when the dialog is dismissed. |
Names auto-detected from defineEmits and source emit() calls; payload and description from __doc.events when present.
Keyboard
- EscCloses the topmost open dialog unless `static` is set.
- Tab / Shift+TabFocus stays within the dialog while open (native top-layer).
- Click backdropDismisses the dialog unless `static` is set.
Related components
Generated from sibling component files that share this documentation route.
Dialog schema formInternal renderer used by dialogForm(schema) to resolve an DomForm children schema from a programmatic dialog.⌄
<DomDialogSchemaForm>Props
Control props
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
schema | array | Array<unknown> | [] | — |
modelValue | object | Record<string, unknown> | {} | — |
submitText | string | string | 'Submit' | — |
cancelText | string | string | 'Cancel' | — |
invalidMessage | string | string | 'Complete the required fields before continuing.' | — |
Auto-generated from Dialog schema form.props and inline _edit hints.
Events
| Name | Payload | Description |
|---|---|---|
| @resolve | Record<string, unknown> | Emitted with validated form values. |
| @dismiss | — | Emitted when the form is cancelled. |
Names auto-detected from defineEmits and source emit() calls; payload and description from __doc.events when present.
Dialog stackRenderer for dialogs created with useDialogs(). Mount it once, then call the dialog API from script.⌄
<DomDialogStack>Props
Control props
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
dialogs | array | Array< | [] | Visible programmatic dialogs from useDialogs(). |
Auto-generated from Dialog stack.props and inline _edit hints.
Slots
| Name | Scope | Description |
|---|---|---|
| #default | { dialog, resolve, dismiss } | Custom renderer for a programmatic dialog body. |
| #footer | { dialog, resolve, dismiss } | Custom renderer for programmatic dialog actions. |
Events
| Name | Payload | Description |
|---|---|---|
| @resolve | ({ id, value }) | Emitted when a dialog resolves. |
| @dismiss | ({ id, value }) | Emitted when a dialog is dismissed. |
| @action | ({ id, dialog, ...payload }) | Emitted by custom dialog bodies for app-specific actions. |
Names auto-detected from defineEmits and source emit() calls; payload and description from __doc.events when present.