Skip to content

Last updated:

How-To: Confirm + Toast Pattern for Destructive Actions

Every update or delete in the CPR admin panel and end-user app uses the same UX pattern:

  1. Update/Delete → opens a ConfirmDialog modal
  2. Confirm → fires the request
  3. Resultvue-sonner toast (success or error)

Creates do not get confirmation — they're additive and easy to undo.

See memory: admin-crud-ux-conventions.

The recipe

vue
<script setup lang="ts">
import { ref } from 'vue'
import { router } from '@inertiajs/vue3'
import { toast } from 'vue-sonner'
import { route } from 'ziggy-js'

const props = defineProps<{ user: User }>()

const showDelete = ref(false)
const target = ref<User | null>(null)

function askDelete(user: User) {
  target.value = user
  showDelete.value = true
}

function confirmDelete() {
  if (!target.value) return
  router.delete(route('admin.users.destroy', target.value.id), {
    preserveScroll: true,
    onSuccess: () => toast.success('User deleted'),
    onError: (errors) => toast.error(errors.message ?? 'Failed to delete'),
    onFinish: () => {
      showDelete.value = false
      target.value = null
    },
  })
}
</script>

<template>
  <button class="text-red-600" @click="askDelete(user)">Delete</button>

  <ConfirmDialog
    v-model="showDelete"
    title="Delete this user?"
    :message="`This will permanently remove ${target?.name}.`"
    confirm-label="Delete"
    variant="danger"
    @confirm="confirmDelete"
  />
</template>

Update with confirmation

For updates that have side effects (deactivate user, cancel reservation, transfer queue ticket), use the same pattern with router.put:

ts
function confirmCancel() {
  router.put(route('admin.reservations.cancel', reservation.value.id), {}, {
    preserveScroll: true,
    onSuccess: () => toast.success('Reservation cancelled'),
    onError: () => toast.error('Failed to cancel'),
    onFinish: () => { showCancel.value = false },
  })
}

Update redirects don't navigate

After update, the admin controller redirects to the edit page, not the index. Inertia treats that as a re-render of the current page, so preserveScroll keeps the scroll position. See memory: admin-crud-ux-conventions.

ConfirmDialog API

ts
defineProps<{
  modelValue: boolean         // v-model
  title: string
  message?: string
  confirmLabel?: string       // default 'Confirm'
  cancelLabel?: string        // default 'Cancel'
  variant?: 'primary' | 'danger' | 'warning'
  loading?: boolean
}>()

defineEmits<{
  'update:modelValue': [boolean]
  confirm: []
  cancel: []
}>()

For a slow operation, bind :loading to a local ref so the confirm button shows a spinner:

ts
const submitting = ref(false)

function confirmDelete() {
  submitting.value = true
  router.delete(route('admin.users.destroy', target.value!.id), {
    onFinish: () => { submitting.value = false; showDelete.value = false },
    // …
  })
}

Toast variants

ts
import { toast } from 'vue-sonner'

toast.success('Saved')
toast.error('Network error')
toast.warning('Capacity nearly reached')
toast.info('Backup started')

Don't toast on create success if the page redirects to a different route — Inertia flash messages already render via <Head> or a global flash banner.

When to skip confirmation

  • Create — no confirmation, redirect to index with flash message
  • Search/filter — no confirmation
  • Toggle that's easy to reverse — small toggle (e.g. is_active) can skip confirmation if it's a single click and reversible

When NOT to use toast

  • Validation errors (422) → render inline next to the field
  • Permission denied (403) → show full-page error (the user got somewhere they shouldn't be)
  • Network errors during table refresh → render a row-level error state, not a toast

Checklist

  • [ ] Update/Delete shows ConfirmDialog before firing
  • [ ] Success toast on onSuccess
  • [ ] Error toast on onError (use errors.message if the backend sent one)
  • [ ] Dialog closes in onFinish (fires for both success and error)
  • [ ] preserveScroll: true so the page doesn't jump
  • [ ] Create does not confirm
  • [ ] Validation errors render inline, not as a toast

CPR - Clinical Patient Records