Recipes
Confirm dialog
A reusable ConfirmDialog component paired with a confirm() helper that returns Promise<boolean>. Covers loading state and destructive variants.
Confirm dialog
The most common modal pattern: ask the user to confirm an action, get a boolean back. This recipe builds a reusable <ConfirmDialog> component and a thin confirm() utility that wraps openModal.
Group registration
main.ts
import { createModal } from '@kolirt/vue-modal'
import type { DefineGroups } from '@kolirt/vue-modal'
declare module '@kolirt/vue-modal' {
interface ModalGroupRegistry extends DefineGroups<['confirm']> {}
}
app.use(createModal())
App.vue
<script setup lang="ts">
import { ModalOverlay, ModalTarget } from '@kolirt/vue-modal'
</script>
<template>
<RouterView />
<ModalTarget group="confirm">
<ModalOverlay class="bg-black/40" />
</ModalTarget>
</template>
<ModalOverlay> is a sibling of the modal stack inside <ModalTarget> — never inside <ModalContent>. It paints behind every modal in the group automatically.
The ConfirmDialog component
<script setup lang="ts">
import { ref } from 'vue'
import {
ModalRoot,
ModalContent,
ModalTitle,
ModalDescription,
useModalContext
} from '@kolirt/vue-modal'
defineOptions({ modalGroup: 'confirm' })
const props = withDefaults(
defineProps<{
title?: string
description?: string
confirmLabel?: string
cancelLabel?: string
destructive?: boolean
/** Async action run before confirming. Returning false aborts. */
onConfirm?: () => Promise<boolean | void>
}>(),
{
title: 'Are you sure?',
description: undefined,
confirmLabel: 'Confirm',
cancelLabel: 'Cancel',
destructive: false,
onConfirm: undefined
}
)
const { close, confirm } = useModalContext<boolean>()
const loading = ref(false)
async function handleConfirm() {
if (props.onConfirm) {
loading.value = true
try {
const result = await props.onConfirm()
if (result === false) return
} finally {
loading.value = false
}
}
confirm(true)
}
</script>
<template>
<ModalRoot class="flex items-center justify-center p-4">
<ModalContent class="w-full max-w-sm rounded-xl bg-white shadow-xl p-6 space-y-4">
<div>
<ModalTitle class="text-lg font-semibold text-gray-900">
{{ props.title }}
</ModalTitle>
<ModalDescription
v-if="props.description"
class="mt-1 text-sm text-gray-500"
>
{{ props.description }}
</ModalDescription>
</div>
<div class="flex justify-end gap-3 pt-2">
<button
class="rounded-lg px-4 py-2 text-sm font-medium text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50 disabled:opacity-50"
:disabled="loading"
@click="close()"
>
{{ props.cancelLabel }}
</button>
<button
class="rounded-lg px-4 py-2 text-sm font-medium text-white disabled:opacity-50 flex items-center gap-2"
:class="props.destructive ? 'bg-red-600 hover:bg-red-700' : 'bg-indigo-600 hover:bg-indigo-700'"
:disabled="loading"
@click="handleConfirm"
>
<svg
v-if="loading"
class="h-4 w-4 animate-spin"
viewBox="0 0 24 24"
fill="none"
>
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
</svg>
{{ props.confirmLabel }}
</button>
</div>
</ModalContent>
</ModalRoot>
</template>
import { openModal, ModalClosedError } from '@kolirt/vue-modal'
import ConfirmDialog from './ConfirmDialog.vue'
export interface ConfirmOptions {
description?: string
confirmLabel?: string
cancelLabel?: string
destructive?: boolean
/** Async action run before the modal resolves. Return false to abort. */
onConfirm?: () => Promise<boolean | void>
}
/**
* confirm('Delete item?', { destructive: true })
* .then(ok => ok && api.delete())
*/
export async function confirm(
title: string,
options: ConfirmOptions = {}
): Promise<boolean> {
return openModal<boolean>(ConfirmDialog, {
props: { title, ...options }
}).catch((e) => {
if (e instanceof ModalClosedError) return false
throw e
})
}
Basic usage
DeleteButton.vue (script)
import { confirm } from '@/utils/confirm'
import { api } from '@/api'
async function deleteItem(id: number) {
if (await confirm('Delete this item?', { destructive: true })) {
await api.items.delete(id)
}
}
Loading state — async confirm
Pass onConfirm to run an async operation inside the modal before it closes. The button shows a spinner; returning false keeps the modal open.
if (
await confirm('Submit report?', {
description: 'This will notify all stakeholders.',
confirmLabel: 'Submit',
onConfirm: async () => {
await api.reports.submit(reportId)
// return false here to abort (e.g. on validation failure)
}
})
) {
toast.success('Report submitted')
}
onConfirm runs inside the modal. Errors thrown there propagate to the caller; the spinner stops automatically via finally.Overlay
The backdrop is rendered by <ModalOverlay> placed inside <ModalTarget> (see the App.vue snippet above). It applies to every modal in that group — you don't include it in each individual modal component.
