Destructive confirm
A confirm that stays disabled until the reader types the name of the thing being deleted.
<script setup lang="ts">
import DestructiveConfirm from '@shared/components/feedback/DestructiveConfirm.vue'
import { Button } from '@shared/components/ui/button'
import { ref } from 'vue'
const open = ref(false)
const deleted = ref(false)
const project = 'warehouse-rollout'
</script>
<template>
<div class="space-y-3">
<Button variant="destructive" :disabled="deleted" @click="open = true">
Delete the project
</Button>
<DestructiveConfirm
v-model:open="open"
title="Delete warehouse-rollout?"
description="Every task, file and comment goes with it. This cannot be undone."
:require-match="project"
confirm-label="Delete project"
@confirm="open = false; deleted = true"
@cancel="open = false"
/>
<p class="text-sm text-muted-foreground">
{{ deleted ? `${project} was deleted` : `Type ${project} in the dialog to enable the button` }}
</p>
</div>
</template>Usage
requireMatch is the exact text the reader has to type. The confirm button is disabled until the input matches it character for character, and Enter in the input confirms once it does.
Use it where an accidental press cannot be walked back: deleting a project, closing an account, dropping a database. Where undo exists, an ordinary confirm dialog is enough, and asking someone to type a name for a reversible action only teaches them to type names.
The input is cleared every time the dialog opens, so a match left over from a previous attempt can never arm the button.
Like the plain confirm, it is fully controlled: the buttons emit, and you close it.
Examples
Through useConfirm
Pass variant: 'destructive' and requireMatch. Both are needed; a destructive variant on its own renders the ordinary dialog in the destructive color.
<script setup lang="ts">
import { Button } from '@shared/components/ui/button'
import { useConfirm } from '@shared/composables/useConfirm'
import { ref } from 'vue'
const { confirm } = useConfirm()
const result = ref('')
async function closeAccount(): Promise<void> {
const ok = await confirm({
title: 'Close the Northwind account?',
description: 'Members lose access immediately and the data is removed after 30 days.',
variant: 'destructive',
requireMatch: 'Northwind',
confirmLabel: 'Close account',
})
result.value = ok ? 'Account closed' : 'Account kept'
}
</script>
<template>
<div class="space-y-3">
<Button variant="destructive" @click="closeAccount">
Close the account
</Button>
<p class="text-sm text-muted-foreground">
{{ result || 'Nothing decided yet' }}
</p>
</div>
</template>Through useConfirm, the heading becomes "Are you sure?" and your title moves into the body, so the words naming the target are still the first thing read.
API
Source: src/shared/components/feedback/DestructiveConfirm.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | required | You own it. |
title | string | required | The heading, drawn in the destructive color. |
description | string | — | The body. |
requireMatch | string | required | The text the reader has to type. Also the input's placeholder. |
confirmLabel | string | 'confirm.confirm' | Defaults to "Confirm". |
cancelLabel | string | 'confirm.cancel' | Defaults to "Cancel". |
Emits
| Event | Payload | Description |
|---|---|---|
update:open | value: boolean | false on Escape or an overlay press. |
confirm | — | Only fires while the typed text matches. |
cancel | — | The cancel button. |
Slots
None.
Accessibility
- The instruction above the input is a real
<label>bound to it, and the text to type is printed inside that label in bold, so the reader is never guessing what to copy. - The confirm button is genuinely
disabledrather than only looking it, so a screen reader reports it as unavailable. Enterin the input confirms, which is what someone who has just typed a name expects.autocomplete="off"keeps the browser from offering a previously typed name and turning the safeguard into one click.- The title and description carry
dir="auto", as on the plain confirm.