Popconfirm

A confirmation bubble that floats out next to its trigger.

Great chapter, looking forward to the next one.

Is this plot different from the original?

Bookmarked.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Button, Inline, Popconfirm, Stack, Text } from '@hina-ui/vue'

  const comments = ref([
    'Great chapter, looking forward to the next one.',
    'Is this plot different from the original?',
    'Bookmarked.',
  ])

  function remove(index: number) {
    comments.value.splice(index, 1)
  }
</script>

<template>
  <Stack gap="sm" align="stretch" class="w-96">
    <Inline
      v-for="(comment, index) in comments"
      :key="comment"
      align="center"
      justify="between"
      gap="sm"
    >
      <Text size="sm">{{ comment }}</Text>
      <Popconfirm
        title="Delete this comment?"
        description="This cannot be undone."
        tone="danger"
        confirm-text="Delete"
        @confirm="remove(index)"
      >
        <Button size="sm" variant="ghost" tone="neutral">Delete</Button>
      </Popconfirm>
    </Inline>
    <Text v-if="!comments.length" tone="muted" size="sm">No comments left.</Text>
  </Stack>
</template>
vue

Usage

import { Popconfirm } from '@hina-ui/vue'
ts

The popconfirm asks one question right next to its trigger, which suits small, local confirmations such as deleting a row or undoing an action; a weighty confirmation that has to interrupt the current task belongs to an AlertDialog. title is the question, description spells out the consequence, and the pair of buttons at the bottom takes cancelText and confirmText for its text. The default slot is the trigger.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Button, Popconfirm, Stack, Text } from '@hina-ui/vue'

  const done = ref(false)
</script>

<template>
  <Stack gap="sm" align="start">
    <Popconfirm title="Mark every notification as read?" @confirm="done = true">
      <Button variant="outline" tone="neutral">Mark all as read</Button>
    </Popconfirm>
    <Text v-if="done" tone="muted" size="sm">Everything was marked as read.</Text>
  </Stack>
</template>
vue

Examples

Destructive actions

tone="danger" gives the confirm button the danger color for irreversible actions such as deleting.

<script setup lang="ts">
  import { Button, Popconfirm } from '@hina-ui/vue'
</script>

<template>
  <Popconfirm
    title="Remove this member?"
    description="They will lose access to this project."
    tone="danger"
    confirm-text="Remove"
  >
    <Button variant="outline" tone="danger">Remove member</Button>
  </Popconfirm>
</template>
vue

Asynchronous confirm

When the confirm handler returns a Promise the bubble waits for it before closing: the confirm button shows a loading indicator, and Cancel, Esc and clicking outside are unavailable meanwhile. If the handler throws, the bubble stays open.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Button, Popconfirm, Stack, Switch, Text } from '@hina-ui/vue'

  const fail = ref(false)
  const status = ref('')

  async function retry() {
    status.value = ''
    await new Promise(resolve => setTimeout(resolve, 1000))
    if (fail.value) {
      status.value = 'Sending failed, please try again later.'
      throw new Error('retry failed')
    }
    status.value = 'Sent again.'
  }
</script>

<template>
  <Stack gap="md" align="start">
    <Switch v-model="fail">Simulate a failure</Switch>
    <Popconfirm title="Send this email again?" confirm-text="Resend" @confirm="retry">
      <Button variant="outline" tone="neutral">Resend</Button>
      <template #content>
        <Text v-if="status" tone="muted" size="sm">{{ status }}</Text>
      </template>
    </Popconfirm>
  </Stack>
</template>
vue

Placement

side and align work as in Popover; the default is right below the trigger, flipping to the other side when space runs out.

<script setup lang="ts">
  import { Button, Inline, Popconfirm } from '@hina-ui/vue'

  const sides = ['top', 'right', 'bottom', 'left'] as const
  const names = { top: 'Top', right: 'Right', bottom: 'Bottom', left: 'Left' }
</script>

<template>
  <Inline gap="sm">
    <Popconfirm v-for="side in sides" :key="side" title="Confirm this placement?" :side="side">
      <Button variant="outline" tone="neutral">{{ names[side] }}</Button>
    </Popconfirm>
  </Inline>
</template>
vue

Controlled

open supports two-way binding, so the bubble can be opened or closed from outside.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Button, Inline, Popconfirm } from '@hina-ui/vue'

  const open = ref(false)
</script>

<template>
  <Inline gap="sm">
    <Button variant="outline" tone="neutral" @click="open = true">Open from outside</Button>
    <Popconfirm
      v-model:open="open"
      title="Discard the draft?"
      description="Everything in the draft is thrown away."
    >
      <Button variant="outline" tone="neutral">Discard draft</Button>
    </Popconfirm>
  </Inline>
</template>
vue

Behavior

  • Focus lands on the Cancel button when the bubble opens, so an accidental Enter does nothing; focus returns to the trigger on close.
  • Esc and clicking outside only close the bubble and do not fire cancel; only the Cancel button does.
  • The page stops scrolling while it is open, and the trigger keeps its pressed look.
  • When the confirm handler returns a Promise the bubble stays busy until it settles and then closes; a thrown error keeps it open.

Accessibility

  • The panel is role="dialog", named after its trigger, with the question and description linked together through aria-describedby.
  • The panel carries aria-busy while busy.
  • Button text comes from the current locale and can be replaced.

API

Props

Prop
Type
Default
Description
title
string
—
Required. The question
description
string
—
Supporting text
confirmText
string
locale
Text of the confirm button
cancelText
string
locale
Text of the cancel button
tone
'accent' | 'danger'
'accent'
Tone of the confirm button
side
'top' | 'right' | 'bottom' | 'left'
'bottom'
Side the bubble floats out on
align
'start' | 'center' | 'end'
'center'
Alignment against the trigger
sideOffset
number
8
Distance from the trigger in pixels
open
boolean
—
Whether it is open, supports two-way binding
class
string
—
Classes appended to the panel

Slots

Slot
Description
default
The trigger
content
Content between the description and buttons

Events

Event
Payload
Description
confirm
—
Confirm was clicked; a Promise from the handler is awaited
cancel
—
Cancel was clicked