Popconfirm

View source
A popover-based action confirmation component.

Introduction

MPopconfirm is a popover-style confirmation component that displays a confirmation bubble before users perform dangerous or irreversible actions. It supports both synchronous and asynchronous confirm callbacks, and fully passes through all UPopover positioning parameters.

Built on Nuxt UI's Popover component

Usage

Place the trigger element in the default slot. Clicking it opens the confirmation bubble:

<template>
  <MPopconfirm title="Confirm Delete?" description="This action cannot be undone.">
    <UButton label="Delete Record" color="neutral" variant="outline" icon="i-lucide-trash" />
  </MPopconfirm>
</template>

type Type

type affects the default icon and confirm button color, with six semantic values:

<template>
  <MPopconfirm type="warning">
    <UButton label="Open" color="neutral" variant="soft" />
  </MPopconfirm>
</template>

icon Icon

icon overrides the default icon from type. Pass any Iconify icon name:

<template>
  <MPopconfirm type="error" icon="i-lucide-trash-2">
    <UButton label="Dangerous Action" color="error" variant="soft" icon="i-lucide-alert-triangle" />
  </MPopconfirm>
</template>

confirmButton Confirm Button

confirmButton accepts full ButtonProps, allowing customization of label, color, icon, and variant:

<template>
  <MPopconfirm
    :confirm-button="{ color: 'error', label: 'Confirm Delete', icon: 'i-lucide-trash-2' }"
  >
    <UButton label="Delete Record" color="error" variant="soft" />
  </MPopconfirm>
</template>

cancelButton Cancel Button

cancelButton accepts ButtonProps or false. Passing false hides the cancel button to force confirmation:

<template>
  <MPopconfirm :cancel-button="false">
    <UButton label="Force Execute" color="warning" variant="soft" />
  </MPopconfirm>
</template>

dismissible Close Strategy

dismissible defaults to false (strict mode). When enabled, clicking the overlay or pressing Esc closes the bubble:

<template>
  <MPopconfirm>
    <UButton label="Close Strategy" color="neutral" variant="outline" />
  </MPopconfirm>
</template>

arrow Arrow

arrow controls the arrow pointing to the trigger. Enabled by default:

<template>
  <MPopconfirm>
    <UButton label="Arrow Toggle" color="neutral" variant="subtle" />
  </MPopconfirm>
</template>

Examples

Async Confirmation

:on-confirm supports returning a Promise. During execution, the confirm button enters a loading state and closes automatically on success:

<script setup lang="ts">
const result = ref('')

async function handleSubmit() {
  await new Promise((resolve) => setTimeout(resolve, 1500))
  result.value = '提交成功'
}
</script>

<template>
  <div class="flex flex-wrap items-center gap-3">
    <MPopconfirm
      type="primary"
      title="确认提交?"
      description="提交后将立即生效,请确认操作。"
      :on-confirm="handleSubmit"
    >
      <UButton color="primary" label="提交申请" icon="i-lucide-send" />
    </MPopconfirm>

    <span v-if="result" class="text-sm text-success">{{ result }}</span>
  </div>
</template>

Body Slot

Use the body slot to insert any content. Pass an empty string to description to hide the default description area:

<script setup lang="ts">
const result = ref('')
</script>

<template>
  <div class="flex flex-wrap items-center gap-3">
    <MPopconfirm
      title="删除用户"
      :description="''"
      :on-confirm="
        () => {
          result = '用户已删除'
        }
      "
    >
      <template #body>
        <div class="flex flex-col gap-2 py-1">
          <p class="text-sm text-muted">即将删除以下用户,操作不可撤销:</p>
          <div class="flex items-center gap-2 rounded-md bg-elevated px-3 py-2">
            <UAvatar size="xs" icon="i-lucide-user" />
            <div class="flex flex-col">
              <span class="text-xs font-medium text-highlighted">张三</span>
              <span class="text-xs text-muted">zhangsan@example.com</span>
            </div>
          </div>
        </div>
      </template>

      <UButton color="error" variant="soft" label="删除用户" icon="i-lucide-user-x" />
    </MPopconfirm>

    <span v-if="result" class="text-sm text-error">{{ result }}</span>
  </div>
</template>

Style Customization

Override the class of internal sections via the ui prop, supporting title, description, footer, content, and more:

<script setup lang="ts">
const result = ref('')
</script>

<template>
  <div class="flex flex-wrap items-center gap-3">
    <MPopconfirm
      type="info"
      title="样式定制示例"
      description="通过 ui 属性覆盖内部各区块的 class,实现精细样式控制。"
      :on-confirm="() => { result = '已确认' }"
      :ui="{
        content: 'w-72',
        title: 'text-info text-base',
        description: 'text-xs text-info/70',
        footer: 'justify-between pt-2 border-t border-default'
      }"
      :confirm-button="{ label: '好的,执行', color: 'info' }"
      :cancel-button="{ label: '不了,取消', variant: 'ghost' }"
    >
      <UButton color="info" variant="soft" label="打开定制气泡" icon="i-lucide-palette" />
    </MPopconfirm>

    <span v-if="result" class="text-sm text-muted">{{ result }}</span>
  </div>
</template>

Popover Direction

Pass the content prop to the underlying UPopover to support top, bottom, left, and right placement:

<script setup lang="ts">
type Side = 'top' | 'bottom' | 'left' | 'right'

const sides: Side[] = ['top', 'bottom', 'left', 'right']
const result = ref('')
</script>

<template>
  <div class="flex flex-wrap items-center gap-2">
    <MPopconfirm
      v-for="side in sides"
      :key="side"
      :content="{ side }"
      :title="`从 ${side} 方弹出`"
      description="透传 Popover 定位参数,支持 top / bottom / left / right。"
      :on-confirm="() => { result = `已从 ${side} 方确认` }"
    >
      <UButton color="neutral" variant="outline" size="sm" :label="side" />
    </MPopconfirm>

    <span v-if="result" class="ml-2 text-sm text-muted">{{ result }}</span>
  </div>
</template>

Error Handling

When the onConfirm callback throws an exception, the popover remains open and fires the @error event where you can display error feedback:

<script setup lang="ts">
const errorMsg = ref('')

const toast = useToast()

async function failingAction() {
  await new Promise(resolve => setTimeout(resolve, 800))
  throw new Error('服务器返回错误:操作被拒绝')
}

function handleError(err: unknown) {
  errorMsg.value = err instanceof Error ? err.message : '未知错误'
  toast.add({
    color: 'error',
    title: '操作失败',
    description: errorMsg.value
  })
}
</script>

<template>
  <div class="flex flex-col gap-3">
    <div class="flex flex-wrap items-center gap-3">
      <MPopconfirm
        type="error"
        title="模拟操作失败"
        description="确认后将触发一个失败的异步操作,弹层保持打开并上报错误。"
        :on-confirm="failingAction"
        @error="handleError"
      >
        <UButton color="error" variant="soft" label="触发失败操作" icon="i-lucide-zap-off" />
      </MPopconfirm>
    </div>
  </div>
</template>

Event Callbacks

Synchronous confirm, cancel, async confirm, and exceptions fire confirm, cancel, and error in sequence:

暂无事件记录

<script setup lang="ts">
const log = ref<string[]>([])

function record(msg: string) {
  log.value = [`[${new Date().toLocaleTimeString()}] ${msg}`, ...log.value].slice(0, 8)
}

async function asyncConfirm() {
  await new Promise(resolve => setTimeout(resolve, 1200))
  record('async confirmed')
}

function rejectedConfirm() {
  throw new Error('被拒绝')
}
</script>

<template>
  <div class="grid grid-cols-1 lg:grid-cols-[1fr_220px] gap-3">
    <div class="flex flex-wrap gap-2">
      <MPopconfirm
        type="warning"
        title="点击确认或取消"
        description="确认会触发 confirm 事件,取消会触发 cancel 事件。"
        :on-confirm="() => record('confirmed')"
        @cancel="record('cancelled')"
      >
        <UButton color="primary" variant="soft" label="同步事件" />
      </MPopconfirm>

      <MPopconfirm
        type="info"
        title="异步事件"
        description="确认按钮等待 onConfirm 异步完成后才触发 confirm 并关闭。"
        :on-confirm="asyncConfirm"
      >
        <UButton color="info" variant="soft" label="异步确认" />
      </MPopconfirm>

      <MPopconfirm
        type="error"
        title="抛错保留"
        description="onConfirm 抛出错误时弹层保持打开,并通过 error 事件暴露异常。"
        :on-confirm="rejectedConfirm"
        @error="(e: unknown) => record(`error: ${(e as Error).message}`)"
      >
        <UButton color="error" variant="outline" label="抛错保留" />
      </MPopconfirm>
    </div>

    <div class="rounded border border-default p-2 text-xs text-muted space-y-1 max-h-40 overflow-auto">
      <p v-if="!log.length">
        暂无事件记录
      </p>
      <p v-for="(item, idx) in log" :key="idx">
        {{ item }}
      </p>
    </div>
  </div>
</template>

API

Props

Prop Default Type
title'确认操作'string

确认气泡的标题文本。

description'请确认是否执行此操作?'string

标题下方的补充说明。 传入空字符串时可隐藏描述区。

type'neutral'"error" | "primary" | "info" | "success" | "warning" | "neutral"

预设的语义化颜色主题,会影响图标。

icon'i-lucide-circle-question-mark'any

标题前展示的图标名称。

confirmButtonOmitByKey<ButtonProps, "loading" | LinkPropsKeys>

透传给确认按钮的属性。 loading 状态由组件内部托管。

cancelButtontrueboolean | ButtonProps

透传给取消按钮的属性。 传入 false 可完全隐藏取消按钮。

mode'click'"click" | "hover"

The display mode of the popover.

content{ side: 'bottom', sideOffset: 8, collisionPadding: 8 }PopoverContentProps & Partial<EmitsToProps<PopoverContentImplEmits>>

The content of the popover.

portaltruestring | false | true | HTMLElement

Render the popover in a portal.

referenceElement | VirtualElement

The reference (or anchor) element that is being referred to for positioning.

Accepts an element or a virtual element (anything with getBoundingClientRect), and can be changed reactively to re-anchor the popover (e.g. for a guided tour). If not provided will use the current component as anchor.

openDelaynumber

The duration from when the mouse enters the trigger until the hover card opens.

closeDelaynumber

The duration from when the mouse leaves the trigger or content until the hover card closes.

arrowtrueboolean

气泡内容与触发器之间的箭头指示。

dismissiblefalseboolean

false 时,点击遮罩层或按下 Esc 键将不会关闭弹层。

modalfalseboolean

The modality of the popover. When set to true, interaction with outside elements will be disabled and only popover content will be visible to screen readers.

uiRecord<string, ClassNameValue> & { content?: SlotClass; arrow?: SlotClass; header?: SlotClass; title?: SlotClass; description?: SlotClass; body?: SlotClass; footer?: SlotClass; icon?: SlotClass; }

Emits

Event Type
confirm[]
cancel[]
error[error: unknown]

Slots

Slot Type
default{ open: boolean; }
header{ close: () => void; }
title{ close: () => void; }
description{ close: () => void; }
actions{ close: () => void; }
body{ close: () => void; }
footer{ close: () => void; }

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    popconfirm: {
      slots: {
        content: 'px-4 py-3 flex flex-col gap-2',
        arrow: '',
        header: 'flex flex-col gap-1.5',
        title: 'flex gap-2 items-center text-sm text-highlighted font-semibold',
        description: 'text-muted text-xs',
        body: '',
        footer: 'mt-1 flex items-center justify-end gap-1.5',
        icon: 'size-4 shrink-0'
      },
      variants: {
        type: {
          primary: {
            icon: 'text-primary'
          },
          info: {
            icon: 'text-info'
          },
          success: {
            icon: 'text-success'
          },
          warning: {
            icon: 'text-warning'
          },
          error: {
            icon: 'text-error'
          },
          neutral: {
            icon: 'text-muted'
          }
        }
      }
    }
  }
})

Changelog

No recent changes
Copyright © 2025 - 2026 YiXuan - MIT License