Skip to main content

Dialogs

Saucebase provides a promise-based useDialog composable that lets any component or module show a confirmation dialog and await the user's response — without managing local state or rendering a <Dialog> inline.

How It Works

  • A singleton DynamicDialog component (resources/js/components/DynamicDialog.vue) is mounted once in App.vue — it is an internal implementation detail; use useDialog() instead of referencing it directly
  • useDialog().confirm(options) opens it and returns a Promise<boolean>
  • true = confirmed, false = cancelled
  • Built on shadcn-vue AlertDialog (role="alertdialog") — clicking outside does not dismiss it, which prevents accidental dismissal of destructive confirmations

Confirm dialog example

Basic Usage

import { useDialog } from '@/composables/useDialog';

const { confirm } = useDialog();

if (await confirm({ title: 'Delete item?' })) {
// user confirmed
}

Destructive Variant

Pass variant: 'destructive' to style the confirm button in red — use this for irreversible actions like deletion or logout. Optionally pass an icon (any Vue component, typically Lucide) to render it above the title in a coloured rounded square.

import { Trash2 } from 'lucide-vue-next';

const { confirm } = useDialog();

if (await confirm({
title: 'Delete item?',
description: 'This action cannot be undone.',
confirmLabel: 'Delete',
cancelLabel: 'Cancel',
variant: 'destructive',
icon: Trash2,
})) {
// user confirmed
}

API Reference

useDialog()

Returns:

PropertyTypeDescription
confirm(opts: ConfirmOptions) => Promise<boolean>Opens the dialog, resolves on close

ConfirmOptions

PropertyTypeDefaultDescription
titlestringDialog heading (required)
descriptionstringOptional body text
confirmLabelstring'Confirm'Confirm button label
cancelLabelstring'Cancel'Cancel button label
variant'default' | 'destructive''default'Confirm button variant
iconComponentOptional Lucide (or any Vue) icon rendered alongside the title
align'center' | 'left''center'center: icon above title, everything centred. left: icon inline-left of title/description

Using in Modules

The composable is part of the core (@/composables/useDialog), so any module can import it directly — no registration needed.

modules/YourModule/resources/js/app.ts
import { useDialog } from '@/composables/useDialog';
import { router } from '@inertiajs/vue3';

const { confirm } = useDialog();

if (await confirm({ title: 'Cancel subscription?', variant: 'destructive' })) {
router.delete(route('billing.subscription.cancel'));
}

Test IDs

The DynamicDialog renders with data-testid attributes for E2E testing:

Elementdata-testid
Dialog wrapperconfirm-dialog
Confirm buttonconfirm-dialog-confirm
Cancel buttonconfirm-dialog-cancel
Playwright example
await page.getByRole('menuitem', { name: /delete/i }).click();

const dialog = page.getByTestId('confirm-dialog');
await expect(dialog).toBeVisible();

await page.getByTestId('confirm-dialog-confirm').click();
await expect(dialog).not.toBeVisible();

What's Next?

  • Navigation — Register action-based nav items that trigger dialogs
  • Modules — Creating and managing modules