Multi-step confirm
Two steps: acknowledge that it cannot be undone, then confirm again.
<script setup lang="ts">
import MultiStepConfirm from '@shared/components/feedback/MultiStepConfirm.vue'
import { Button } from '@shared/components/ui/button'
import { ref } from 'vue'
const open = ref(false)
const wiped = ref(false)
</script>
<template>
<div class="space-y-3">
<Button variant="destructive" :disabled="wiped" @click="open = true">
Reset the workspace
</Button>
<MultiStepConfirm
v-model:open="open"
title="Reset this workspace?"
description="Every project, member and integration is removed and the workspace goes back to how it shipped."
confirm-label="Reset everything"
@confirm="open = false; wiped = true"
@cancel="open = false"
/>
<p class="text-sm text-muted-foreground">
{{ wiped ? 'The workspace was reset' : 'Tick the box, then confirm again' }}
</p>
</div>
</template>Usage
Step one is a checkbox reading "I understand this action cannot be undone." Continue stays disabled until it is ticked. Step two says "This is your last chance to back out" and names the button that will do it.
Both steps reset every time the dialog opens, so a tick left over from a previous attempt cannot skip the acknowledgement.
Reach for it where the action is broad rather than specific: resetting a whole workspace, rotating every key, wiping an environment. Where the thing has a name, the destructive confirm asking the reader to type it is the stronger guard, because it proves they know which thing.
Examples
Through useConfirm
variant: 'multi-step' is all it takes. Unlike the destructive variant, the title is not rewritten: it stays as the heading.
<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 rotateKeys(): Promise<void> {
const ok = await confirm({
title: 'Rotate every API key?',
description: 'All existing keys stop working at once. Anything using them fails until it is updated.',
variant: 'multi-step',
confirmLabel: 'Rotate keys',
})
result.value = ok ? 'Keys rotated' : 'Keys left alone'
}
</script>
<template>
<div class="space-y-3">
<Button variant="destructive" @click="rotateKeys">
Rotate the API keys
</Button>
<p class="text-sm text-muted-foreground">
{{ result || 'Nothing decided yet' }}
</p>
</div>
</template>API
Source: src/shared/components/feedback/MultiStepConfirm.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. |
confirmLabel | string | 'confirm.confirm' | The final button, and the name printed in the last-chance line. |
cancelLabel | string | 'confirm.cancel' | Defaults to "Cancel". |
Emits
| Event | Payload | Description |
|---|---|---|
update:open | value: boolean | false on Escape or an overlay press. |
confirm | — | Only from the second step. |
cancel | — | The cancel button, on either step. |
Slots
None.
Accessibility
- The acknowledgement is a checkbox inside a
<label>, so the whole row is the hit area and the sentence is the checkbox's name. - Continue is genuinely
disableduntil the box is ticked. - Cancel stays available on both steps, so there is always a way out that is not Escape.
- The last-chance line names the exact button that follows, rather than saying "the button below".
- The title and description carry
dir="auto", as on the plain confirm.