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>
)
}<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>Component API
| Prop | Default | Description |
|---|---|---|
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". |
cancelLabel | Cancel | Label of the plain button. |
color | red | Color of the confirm button. |
size | md | Forwarded to the Alert. |
disabled | false | Keeps the confirm button disabled. |
| Prop | Default | Description |
|---|---|---|
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". |
cancelLabel | Cancel | Label of the plain button. |
color | red | Color of the confirm button. |
size | md | Forwarded to the Alert. |
disabled | false | Keeps 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" …>