PXD

Confirm Popover

A popover that asks for a second confirmation before running an action. The trigger is whatever you put in the default slot, and the popover opens above it with a confirm and a cancel button.

Default

<script setup>
function handleConfirm() {
  console.log('confirmed')
}
</script>

<template>
  <PConfirmPopover
    title="Delete this file?"
    description="This action cannot be undone."
    @confirm="handleConfirm"
  >
    <PButton variant="error">Delete</PButton>
  </PConfirmPopover>
</template>

Destructive

<template>
  <PConfirmPopover
    title="Delete 12 items?"
    description="Every selected item will be removed."
    confirm-variant="error"
    confirm-text="Delete"
    cancel-text="Keep"
  >
    <PButton variant="error">Delete selected</PButton>
  </PConfirmPopover>
</template>

Async confirmation

Pass before-confirm to run an async task while the confirm button stays in its loading state. Returning false or rejecting keeps the popover open.

<script setup>
async function deleteItem() {
  await new Promise((resolve) => setTimeout(resolve, 800))
}

function handleConfirm() {
  console.log('deleted')
}
</script>

<template>
  <PConfirmPopover
    title="Delete this file?"
    :before-confirm="deleteItem"
    @confirm="handleConfirm"
  >
    <PButton>Delete</PButton>
  </PConfirmPopover>
</template>

Custom actions

Replace the buttons with the actions slot — for example a hold button that only confirms after a long press.

<template>
  <PConfirmPopover title="Hold to delete">
    <PButton variant="error">Delete</PButton>

    <template #actions="{ confirm }">
      <PHoldButton size="sm" variant="error" @confirm="confirm">Hold to delete</PHoldButton>
    </template>
  </PConfirmPopover>
</template>

Multiple triggers

trigger-selector lets any number of DOM elements share one confirm popover, so a list or tree can render a single instance. The popover follows the matched trigger, and the content / actions slots receive the active one — switching triggers while a before-confirm is still pending discards that confirm.

<template>
  <PConfirmPopover trigger-selector="[data-confirm-file]">
    <PStack direction="vertical" align="start" class="w-full gap-1">
      <button
        v-for="file in ['report.pdf', 'photo.png', 'notes.md']"
        :key="file"
        type="button"
        data-confirm-file
        :data-file="file"
        class="h-8 w-full rounded-md border px-2 text-start text-sm bg-background-100"
      >
        {{ file }}
      </button>
    </PStack>

    <template #content="{ activeTrigger }">
      <div class="p-3 flex max-w-64 flex-col gap-2">
        <p class="m-0 text-sm font-medium">Delete {{ activeTrigger?.dataset.file }}?</p>
        <p class="m-0 text-[13px] text-foreground-secondary">This action cannot be undone.</p>
      </div>
    </template>

    <template #actions="{ confirm, cancel, pending }">
      <div class="gap-1.5 flex items-center justify-end">
        <PButton size="sm" variant="ghost" :disabled="pending" @click="cancel">
          Cancel
        </PButton>

        <PButton size="sm" variant="error" :loading="pending" @click="confirm">
          Delete
        </PButton>
      </div>
    </template>
  </PConfirmPopover>
</template>

Props

NameTypeDefaultDescription
model-valueboolean-Controlled visibility; omit it to let the component track visibility on its own
titlestring-Title rendered above the description, also used as the dialog label
descriptionstring-Secondary line rendered between the title and the actions
confirm-textstring-Label of the confirm button, defaulting to locale.interaction.confirm
cancel-textstring-Label of the cancel button, defaulting to locale.interaction.cancel
confirm-variant'default' | 'primary' | 'error' | 'warning' | 'success' | 'ghost' | 'simple' | 'link'primaryVariant of the confirm button
position'top' | 'right' | 'bottom' | 'left' | ...topPreferred placement, flipped when there is no room; -start and -end align to the edge
disabledboolean-Ignore every trigger event so the popover cannot be shown
trigger-selectorstring-Selector for multiple DOM triggers inside the default slot; every matched element shares this popover
before-confirm() => Awaitable<boolean | void>-Ran before confirming; return false or reject to keep the popover open

Events

NameTypeDescription
confirm() => voidEmitted after before-confirm settles without returning false.
cancel() => voidEmitted when the cancel button is clicked.
update:model-value(visible: boolean) => voidEmitted when the visibility changes.

Slots

NameDescription
defaultTrigger content.
contentReplaces the whole popover body. Slot props: confirm: () => void, cancel: () => void, pending: boolean, activeTrigger: HTMLElement | null, activeTriggerIndex: number.
actionsReplaces only the buttons. Slot props: confirm: () => void, cancel: () => void, pending: boolean, activeTrigger: HTMLElement | null, activeTriggerIndex: number.

Source

Source