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.

Dialog title

Supporting text shown under the heading.

Dialog body content.

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

Delete project?

This will permanently delete the project and all of its data.

There is no undo. Type the project name to confirm.

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

Terms and conditions

Read the full agreement before accepting.

Account access

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.

Acceptable use

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.

Customer content

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.

Service changes

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.

Billing

Paid plans renew automatically unless cancelled before the renewal date. Taxes, usage charges, and subscription changes may be reflected on future invoices.

Privacy

Personal data is handled according to the privacy notice. Administrative users should make sure their teams understand what information is submitted to the service.

Availability

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.

Termination

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.

By accepting, you confirm that you have reviewed the terms above and are authorised to agree on behalf of your workspace.

Scroll to the end to continue.

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

Programmatic control

No trigger slot — open from script via the component ref.

Call open(), close(), or toggle() on the ref, or bind v-model.

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

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

Delete project?

This action cannot be undone.

All issues, comments, and uploads will be removed.

Final confirmation

Type the project name to confirm deletion.

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

Delete project?

This will permanently delete the project and all of its data.

There is no undo. Type the project name to confirm.

Custom.vuevue
<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 dialog

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

NameTypeTSDefaultDescription
modelValuebooleanbooleanfalseWhether the dialog is open.
titlestringstring''Heading rendered inside the dialog card.
descriptionstringstring''Supporting copy under the heading.
staticbooleanbooleanfalseModal cannot be dismissed by backdrop click or Esc — use footer actions with data-close or v-model.
backdropbooleanbooleantrueShow the dimmed, blurred page backdrop behind the dialog.
footerbooleanbooleantrueShow the footer region and default Close action when no footer slot is provided.

Auto-generated from Dialog.props and inline _edit hints.

Slots

NameScopeDescription
#triggerOptional. 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.
#footerAction buttons. Add `data-close` on a control to dismiss the dialog.

Events

NamePayloadDescription
@update:modelValue(open
open: boolean;
: boolean)
Emitted when the dialog opens or closes.
@closeEmitted 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

NameTypeTSDefaultDescription
schemaarrayArray<unknown>[]
modelValueobjectRecord<string, unknown>{}
submitTextstringstring'Submit'
cancelTextstringstring'Cancel'
invalidMessagestringstring'Complete the required fields before continuing.'

Auto-generated from Dialog schema form.props and inline _edit hints.

Events

NamePayloadDescription
@resolveRecord<string, unknown>Emitted with validated form values.
@dismissEmitted 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

NameTypeTSDefaultDescription
dialogs
[
	{
		id: "dialog-1",
		title: "Delete project?",
		description: "This action cannot be undone.",
		message: "Are you sure?",
		type: "dialog",
	}
]
arrayArray<DialogsItem
type DialogsItem = {
	id?: string; // ID
	title?: string; // Title
	description?: string; // Description
	message?: string; // Message
	type?: string; // Type
};
>
[]Visible programmatic dialogs from useDialogs().

Auto-generated from Dialog stack.props and inline _edit hints.

Slots

NameScopeDescription
#default{ dialog, resolve, dismiss }Custom renderer for a programmatic dialog body.
#footer{ dialog, resolve, dismiss }Custom renderer for programmatic dialog actions.

Events

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