Skip to content
  1. Overlays
  2. Confirm dialog

Confirm dialog

The destructive confirmation: a question, what breaks and what survives, then Cancel and the verb.

'use client'

import { Button } from '@/components/button'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { useState } from 'react'

export default function Example() {
  let [open, setOpen] = useState(false)
  let [revoked, setRevoked] = useState(false)

  return (
    <>
      <Button color="red" disabled={revoked} onClick={() => setOpen(true)}>
        {revoked ? 'Key revoked' : 'Revoke key'}
      </Button>
      <ConfirmDialog
        open={open}
        onClose={() => setOpen(false)}
        onConfirm={() => {
          setRevoked(true)
          setOpen(false)
        }}
        title="Revoke Production server?"
        description="Any integration using this key will immediately stop working. This action cannot be undone."
        confirmLabel="Revoke key"
      />
    </>
  )
}
<script setup lang="ts">
import Button from '@/components/button.vue'
import ConfirmDialog from '@/components/confirm-dialog.vue'
import { ref } from 'vue'

const open = ref(false)
const revoked = ref(false)

function revoke() {
  revoked.value = true
  open.value = false
}
</script>

<template>
  <Button color="red" :disabled="revoked" @click="open = true">{{ revoked ? 'Key revoked' : 'Revoke key' }}</Button>
  <ConfirmDialog
    :open="open"
    title="Revoke Production server?"
    description="Any integration using this key will immediately stop working. This action cannot be undone."
    confirm-label="Revoke key"
    @close="open = false"
    @confirm="revoke"
  />
</template>

Installation

Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.

import type React from 'react'
import { Alert, AlertActions, AlertDescription, AlertTitle } from './alert'
import { Button } from './button'

export function ConfirmDialog({
  open,
  onClose,
  onConfirm,
  title,
  description,
  confirmLabel,
  cancelLabel = 'Cancel',
  color = 'red',
  size,
  disabled = false,
  children,
}: {
  open: boolean
  onClose: () => void
  onConfirm: () => void
  title: React.ReactNode
  description?: React.ReactNode
  confirmLabel: React.ReactNode
  cancelLabel?: React.ReactNode
  color?: React.ComponentProps<typeof Button>['color']
  size?: React.ComponentProps<typeof Alert>['size']
  disabled?: boolean
  children?: React.ReactNode
}) {
  return (
    <Alert open={open} onClose={() => onClose()} size={size}>
      <AlertTitle>{title}</AlertTitle>
      {(children || description) && <AlertDescription>{children ?? description}</AlertDescription>}
      <AlertActions>
        <Button plain onClick={onClose}>
          {cancelLabel}
        </Button>
        <Button color={color} disabled={disabled} onClick={onConfirm}>
          {confirmLabel}
        </Button>
      </AlertActions>
    </Alert>
  )
}

Also copy Alert, Button.

<script setup lang="ts">
import Alert from './alert.vue'
import AlertActions from './alert-actions.vue'
import AlertDescription from './alert-description.vue'
import AlertTitle from './alert-title.vue'
import Button from './button.vue'

type AlertSize = InstanceType<typeof Alert>['$props']['size']
type ButtonColor = InstanceType<typeof Button>['$props']['color']

withDefaults(
  defineProps<{
    open: boolean
    title: string
    description?: string
    confirmLabel: string
    cancelLabel?: string
    color?: ButtonColor
    size?: AlertSize
    disabled?: boolean
  }>(),
  { cancelLabel: 'Cancel', color: 'red', disabled: false }
)

const emit = defineEmits<{ close: []; confirm: [] }>()
</script>

<template>
  <Alert :open="open" :size="size" @close="emit('close')">
    <AlertTitle>{{ title }}</AlertTitle>
    <AlertDescription v-if="$slots.default || description">
      <slot>{{ description }}</slot>
    </AlertDescription>
    <AlertActions>
      <Button plain @click="emit('close')">{{ cancelLabel }}</Button>
      <Button :color="color" :disabled="disabled" @click="emit('confirm')">{{ confirmLabel }}</Button>
    </AlertActions>
  </Alert>
</template>

Also copy Alert, Button.

Component API

PropDefaultDescription
ConfirmDialogbuilt on the <Alert> component
open-Whether the dialog is open.
onClose-Called on Cancel, Escape and outside click.
onConfirm-Called when the confirm button is pressed. Close the dialog yourself once the action is done.
title-A question naming the record: "Revoke Production server?"
description-Plain text body. Pass children instead for rich content.
confirmLabel-Verb and object: "Revoke key". Never "OK" or "Yes".
cancelLabelCancelLabel of the plain button.
colorredColor of the confirm button.
sizemdForwarded to the Alert.
disabledfalseKeeps the confirm button disabled.
PropDefaultDescription
ConfirmDialogbuilt on the <Alert> component
open-Whether the dialog is open.
@close-Emitted on Cancel, Escape and outside click.
@confirm-Emitted when the confirm button is pressed. Close the dialog yourself once the action is done.
title-A question naming the record: "Revoke Production server?"
description-Plain text body. Use the default slot instead for rich content.
confirmLabel-Verb and object: "Revoke key". Never "OK" or "Yes".
cancelLabelCancelLabel of the plain button.
colorredColor of the confirm button.
sizemdForwarded to the Alert.
disabledfalseKeeps the confirm button disabled.

Examples

Basic example

The title asks, the description says what stops working and whether it can be undone, the confirm button names the action. Cancel always comes first:

'use client'

import { Button } from '@/components/button'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { useState } from 'react'

export default function Example() {
  let [open, setOpen] = useState(false)
  let [revoked, setRevoked] = useState(false)

  return (
    <>
      <Button color="red" disabled={revoked} onClick={() => setOpen(true)}>
        {revoked ? 'Key revoked' : 'Revoke key'}
      </Button>
      <ConfirmDialog
        open={open}
        onClose={() => setOpen(false)}
        onConfirm={() => {
          setRevoked(true)
          setOpen(false)
        }}
        title="Revoke Production server?"
        description="Any integration using this key will immediately stop working. This action cannot be undone."
        confirmLabel="Revoke key"
      />
    </>
  )
}
<script setup lang="ts">
import Button from '@/components/button.vue'
import ConfirmDialog from '@/components/confirm-dialog.vue'
import { ref } from 'vue'

const open = ref(false)
const revoked = ref(false)

function revoke() {
  revoked.value = true
  open.value = false
}
</script>

<template>
  <Button color="red" :disabled="revoked" @click="open = true">{{ revoked ? 'Key revoked' : 'Revoke key' }}</Button>
  <ConfirmDialog
    :open="open"
    title="Revoke Production server?"
    description="Any integration using this key will immediately stop working. This action cannot be undone."
    confirm-label="Revoke key"
    @close="open = false"
    @confirm="revoke"
  />
</template>

Rich description

Pass children instead of description when the body needs emphasis or a link:

Use the default slot instead of description when the body needs emphasis or a link:

<ConfirmDialog
  open={open}
  onClose={() => setOpen(false)}
  onConfirm={() => setOpen(false)}
  title="Delete Mariam Keita?"
  confirmLabel="Delete customer"
>
  They leave the customer list and stop appearing in search. Past payments and invoices
  are kept, and you can restore the profile from the{' '}
  <strong className="font-medium text-zinc-950 dark:text-white">Archived only</strong> filter.
</ConfirmDialog>
<template>
  <ConfirmDialog :open="open" title="Delete Mariam Keita?" confirm-label="Delete customer" @close="open = false" @confirm="open = false">
    They leave the customer list and stop appearing in search. Past payments and invoices
    are kept, and you can restore the profile from the
    <strong class="font-medium text-zinc-950 dark:text-white">Archived only</strong> filter.
  </ConfirmDialog>
</template>

Driven by a target

For a list, hold the record to act on in state. One dialog serves every row, and the record stays set while the dialog fades out:

  • Kofi Mensah
  • Ines Carvalho
  • Lina Mansouri
let [target, setTarget] = useState<Member | null>(null)
let [open, setOpen] = useState(false)

<ConfirmDialog
  open={open}
  onClose={() => setOpen(false)}
  onConfirm={() => {
    remove(target)
    setOpen(false)
  }}
  title={`Remove ${target?.name}?`}
  description="They will immediately lose access to this organization and its dashboard. This action cannot be undone."
  confirmLabel="Remove member"
/>
<script setup lang="ts">
const target = ref<Member | null>(null)
const open = ref(false)

function confirm() {
  remove(target.value)
  open.value = false
}
</script>

<template>
  <ConfirmDialog
    :open="open"
    :title="`Remove ${target?.name}?`"
    description="They will immediately lose access to this organization and its dashboard. This action cannot be undone."
    confirm-label="Remove member"
    @close="open = false"
    @confirm="confirm"
  />
</template>

Larger dialog

size is forwarded to the Alert, for a longer explanation:

<ConfirmDialog
  size="lg"
  title="Archive M-Pesa business till?"
  description="The account stops receiving payouts and disappears from this list. Past transfers keep pointing to it, and you can restore it from Beneficiaries."
  confirmLabel="Archive account"
  …
/>
<template>
  <ConfirmDialog
    size="lg"
    title="Archive M-Pesa business till?"
    description="The account stops receiving payouts and disappears from this list. Past transfers keep pointing to it, and you can restore it from Beneficiaries."
    confirm-label="Archive account"
    …
  />
</template>

Confirmation with a field

When the action needs a reason or a password, it is a form, not an alert: use the Form dialog with color="red" so the record keeps its field semantics.

See the Form dialog page, destructive form example.

<FormDialog color="red" submitLabel="Block customer" …>
<FormDialog color="red" submit-label="Block customer" …>