Skip to content

useConfirm ​

Ask a yes-or-no question from anywhere and await the answer. No dialog state in your component.

Open in demo

Import ​

ts
import { useConfirm } from '@shared/composables/useConfirm'

Signature ​

ts
function useConfirm(): {
  confirm: (opts: ConfirmOptions) => Promise<boolean>
}

interface ConfirmOptions {
  title: string
  description?: string
  variant?: 'default' | 'destructive' | 'multi-step'
  requireMatch?: string
  confirmLabel?: string
  cancelLabel?: string
}

ConfirmOptions ​

KeyTypeDescription
titlestringRequired. A translation key or a literal.
descriptionstringThe line under it.
variant'default' | 'destructive' | 'multi-step'Which dialog the host renders.
requireMatchstringWith destructive, the text the reader has to type before Confirm enables.
confirmLabelstringDefaults to "Confirm".
cancelLabelstringDefaults to "Cancel".

Which dialog you get ​

variantPlusRenders
'default' or omitted—Confirm dialog
'destructive'—Confirm dialog in the destructive color
'destructive'requireMatchDestructive confirm
'multi-step'—Multi-step confirm

Example ​

vue
<script setup lang="ts">
import { useConfirm } from '@shared/composables/useConfirm'
import { ref } from 'vue'

interface Project {
  id: number
  name: string
}

const project = ref<Project>({ id: 4, name: 'Northwind migration' })
const removed = ref(false)

const { confirm } = useConfirm()

async function signOut() {
  const ok = await confirm({
    title: 'Sign out?',
    description: 'You will be returned to the login screen.',
    confirmLabel: 'Sign out',
  })
  if (ok)
    console.log('signing out')
}

async function remove() {
  const ok = await confirm({
    title: `Delete ${project.value.name}?`,
    description: 'This cannot be undone.',
    variant: 'destructive',
    requireMatch: project.value.name,
  })
  removed.value = ok
}
</script>

<template>
  <div class="flex flex-wrap gap-2">
    <button type="button" @click="signOut()">
      Sign out
    </button>
    <button type="button" @click="remove()">
      Delete project
    </button>
    <span v-if="removed">Deleted.</span>
  </div>
</template>

Notes ​

  • One host, not one app per call. ConfirmHost is mounted once in App.vue, so a dialog inherits the app's context — i18n, the theme, the router — and can play its exit animation. Spinning up a createApp per confirmation gets none of that.
  • A second confirm() while one is open cancels the first, resolving it false. It does not queue and it does not throw, so two overlapping flows cannot leave a promise hanging forever.
  • A destructive request is retitled at the host: the heading becomes "Are you sure?" and your title drops into the body, where it names the target. That is done once, centrally, so every call site keeps passing title: 'Delete Northwind migration?'. A default confirm is left alone, because it is already a question.
  • The dialog resolves false on Escape and on an overlay press, so there is no path where the promise never settles.
  • Labels default to translation keys, so they follow the language switcher. A string you pass wins.