Hacktron Design System

Dialog

A window over the page that holds the user until they close it.

<template>  <Dialog v-model:open="open" title="Delete repository" description="This can't be undone.">    <template #footer>      <Button variant="destructive" @click="onConfirm">Delete</Button>    </template>  </Dialog></template>

Props

Name Type Default
openboolean (v-model:open)false
titlestring-
descriptionstring-
closeboolean | objecttrue
fullscreenbooleanfalse

Slots

Name Description
bodyThe dialog's main content.
footerAction buttons, right-aligned by default (justify-end) - matches apps/frontend's own confirm-modal.vue: cancel as color="neutral" variant="ghost", the primary action last in its default solid color (or color="error" for a destructive confirm).
titleEscape hatch: replaces the `title` prop's plain text.
descriptionEscape hatch: replaces the `description` prop's plain text.

Guidelines

Do

  • Use title/description props (or the #title/#description slots when they need more than plain text) instead of hand-building a header inside #body - that's the pattern every real modal in apps/frontend had to repeat by hand because there was no shared wrapper to fix the background color once.
  • Put action buttons directly in the #footer slot - it's already right-aligned with an 8px gap by default, no wrapping div needed. Put the least destructive action first: cancel/dismiss as variant="ghost", the primary action last in its default solid color. This matches the real confirm-modal.vue pattern already used by 27 call sites in apps/frontend.
  • For a destructive confirmation specifically, use variant="destructive" on the confirm button - the same default apps/frontend's shared confirm modal already uses.
  • Bind v-model:open to a local ref(false) - every real usage does this; none rely on a default-slot trigger or purely imperative open/close.

Don't

  • Don't hand-build a close (×) button in the header - close is true by default (matching what every real modal already renders, just via a manually-built button instead of the native one) and already positioned/styled correctly; only pass close when you actually want to hide it.
  • Don't pass a fixed pixel width (:ui="{ content: 'w-[500px]' }") unless the content genuinely needs it - most real modals just take the stock max-w-lg, and the few that don't are the exception, not the rule.
  • Don't reach for Dialog for something the user should be able to dismiss and forget about (a save confirmation, an undo prompt) - use a toast (useAppToast()) instead; a modal demands the user's attention before they can do anything else.
  • Don't expect bg-surface-popover - despite that being what most real modals in apps/frontend happened to use, Dialog uses bg-surface-background instead, plus a plain 16px gap between header/body/footer (no divider line) and explicit title/description type sizes that don't match Nuxt UI's own stock modal styles at all.

Examples

Basic

A modal for something the user can safely dismiss and return to later.

const open = ref(false)<Button variant="secondary" @click="open = true">Edit profile</Button><Dialog v-model:open="open" title="Edit profile" description="Make changes to your profile here. Click save when you're done.">  <template #body>    <Input placeholder="Display name" class="w-full" />  </template>  <template #footer>    <Button variant="primary" @click="open = false">Save changes</Button>  </template></Dialog>

Destructive confirm

A dialog housing just the one destructive action, with no separate cancel button.

const open = ref(false)<Button variant="secondary" @click="open = true">Delete repository</Button><Dialog v-model:open="open" title="Delete repository" description="This will permanently delete the repository and all of its scan history. This can't be undone.">  <template #footer>    <Button variant="destructive" @click="open = false">Delete</Button>  </template></Dialog>