Beta, expect API change, and bugs. Hit one? Tell us.

Skip to content

Alert Dialog

A dialog requiring a response.

<script setup lang="ts">
import { AlertDialog } from '@shardsui/vue/alert-dialog'
</script>

<template>
  <AlertDialog.Root>
    <AlertDialog.Trigger
      class="flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 text-sm font-normal text-red-800 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
    >
      Discard draft
    </AlertDialog.Trigger>
    <AlertDialog.Portal>
      <AlertDialog.Backdrop
        class="fixed inset-0 min-h-dvh bg-black opacity-20 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-[-webkit-touch-callout:none]:absolute"
      />
      <AlertDialog.Popup
        class="fixed top-1/2 left-1/2 -mt-8 w-96 max-w-[calc(100vw-3rem)] -translate-1/2 rounded-lg bg-gray-50 p-4 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[scale,opacity] duration-100 ease-out data-ending-style:scale-[0.98] data-ending-style:opacity-0 data-starting-style:scale-[0.98] data-starting-style:opacity-0"
      >
        <AlertDialog.Title class="mb-1 text-base font-semibold">Discard draft?</AlertDialog.Title>
        <AlertDialog.Description class="mb-4 text-sm text-gray-600">
          This can't be undone.
        </AlertDialog.Description>
        <div class="flex justify-end gap-3">
          <AlertDialog.Close
            class="flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 text-sm font-normal text-gray-900 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
          >
            Cancel
          </AlertDialog.Close>
          <AlertDialog.Close
            class="flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 text-sm font-normal text-red-800 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
          >
            Discard
          </AlertDialog.Close>
        </div>
      </AlertDialog.Popup>
    </AlertDialog.Portal>
  </AlertDialog.Root>
</template>

Anatomy

<script setup>
import { AlertDialog } from '@shardsui/vue/alert-dialog'
</script>

<template>
  <AlertDialog.Root>
    <AlertDialog.Trigger />
    <AlertDialog.Portal>
      <AlertDialog.Backdrop />
      <AlertDialog.Viewport>
        <AlertDialog.Popup>
          <AlertDialog.Title />
          <AlertDialog.Description />
          <AlertDialog.Close />
        </AlertDialog.Popup>
      </AlertDialog.Viewport>
    </AlertDialog.Portal>
  </AlertDialog.Root>
</template>

Examples

Open from a menu

To open an alert dialog from a menu, keep the alert dialog controlled and flip its state from the menu item's @click handler.

<script setup>
import { AlertDialog } from '@shardsui/vue/alert-dialog'
import { Menu } from '@shardsui/vue/menu'
import { shallowRef } from 'vue'

const dialogOpen = shallowRef(false)
</script>

<template>
  <Menu.Root>
    <Menu.Trigger>Open menu</Menu.Trigger>
    <Menu.Portal>
      <Menu.Positioner>
        <Menu.Popup>
          <!-- Open the dialog when the menu item is clicked -->
          <Menu.Item @click="dialogOpen = true">Open dialog</Menu.Item>
        </Menu.Popup>
      </Menu.Positioner>
    </Menu.Portal>
  </Menu.Root>

  <!-- Control the dialog state -->
  <AlertDialog.Root v-model:open="dialogOpen">
    <AlertDialog.Portal>
      <AlertDialog.Backdrop />
      <AlertDialog.Popup>
        <!-- Rest of the dialog -->
      </AlertDialog.Popup>
    </AlertDialog.Portal>
  </AlertDialog.Root>
</template>

Close confirmation

A nested confirmation dialog guards against losing work: it opens when the text typed into the parent dialog is about to be discarded.

Veto the close by controlling open and deciding in the @update:open handler. When a close is requested the handler runs; if you don't commit the new value, the prop keeps the old one and the dialog stays open. Open the confirmation there instead, so the prompt appears whether the user presses Esc or hits a close button. An alert dialog is never dismissed by clicking the backdrop.

<template>
  <AlertDialog.Root
    :open="open"
    @update:open="
      (next) => {
        if (!next && hasUnsavedChanges) return // veto: don't commit, dialog stays open
        open = next
      }
    "
  >
    ...
  </AlertDialog.Root>
</template>

Style the parent dialog through the [data-nested-dialog-open] selector and the var(--nested-dialogs) CSS variable. Child dialogs render their own backdrop, marked with data-nested. Hide it with [data-nested] { opacity: 0 } to keep the parent visible behind the one on top.

The demo below uses Dialog. The same pattern applies to AlertDialog.

<script setup lang="ts">
import { AlertDialog } from '@shardsui/vue/alert-dialog'
import { Dialog } from '@shardsui/vue/dialog'
import { shallowRef, useId } from 'vue'

const dialogOpen = shallowRef(false)
const confirmationOpen = shallowRef(false)
const noteValue = shallowRef('')
const titleId = useId()

function requestOpenChange(open: boolean) {
  // Veto the close by not committing; prompt instead.
  if (!open && noteValue.value) {
    confirmationOpen.value = true
    return
  }
  noteValue.value = ''
  dialogOpen.value = open
}
</script>

<template>
  <Dialog.Root :open="dialogOpen" @update:open="requestOpenChange">
    <Dialog.Trigger
      class="flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 text-sm font-normal text-gray-900 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
    >
      Add note
    </Dialog.Trigger>
    <Dialog.Portal>
      <Dialog.Backdrop
        class="fixed inset-0 min-h-dvh bg-black opacity-20 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-[-webkit-touch-callout:none]:absolute"
      />
      <Dialog.Popup
        class="fixed top-[calc(50%+1.25rem*var(--nested-dialogs))] left-1/2 -mt-8 flex w-96 max-w-[calc(100vw-3rem)] -translate-1/2 scale-[calc(1-0.1*var(--nested-dialogs))] flex-col gap-1 rounded-lg bg-gray-50 p-4 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[top,scale,opacity] duration-100 ease-out after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:bg-black/5 after:opacity-0 after:transition-opacity after:duration-100 after:ease-out data-ending-style:top-[calc(50%+0.25rem+1.25rem*var(--nested-dialogs))] data-ending-style:scale-[0.96] data-ending-style:opacity-0 data-nested-dialog-open:after:opacity-100 data-starting-style:top-[calc(50%+0.25rem+1.25rem*var(--nested-dialogs))] data-starting-style:scale-[0.96] data-starting-style:opacity-0"
      >
        <Dialog.Title :id="titleId" class="text-base font-semibold">Note</Dialog.Title>
        <form class="flex flex-col gap-4" @submit.prevent="dialogOpen = false">
          <textarea
            v-model="noteValue"
            :aria-labelledby="titleId"
            required
            class="min-h-32 w-full rounded-md border border-gray-200 p-2 text-sm font-normal text-gray-900 focus:outline-2 focus:-outline-offset-1 focus:outline-gray-950 any-pointer-coarse:text-base"
            placeholder="Capture a key takeaway…"
          ></textarea>
          <div class="flex justify-end gap-3">
            <Dialog.Close
              class="flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 text-sm font-normal text-gray-900 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
            >
              Cancel
            </Dialog.Close>
            <button
              type="submit"
              class="flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 text-sm font-normal text-gray-900 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
            >
              Save
            </button>
          </div>
        </form>
      </Dialog.Popup>
    </Dialog.Portal>

    <AlertDialog.Root v-model:open="confirmationOpen">
      <AlertDialog.Portal>
        <AlertDialog.Popup
          class="fixed top-[calc(50%+1.25rem*var(--nested-dialogs))] left-1/2 -mt-8 flex w-96 max-w-[calc(100vw-3rem)] -translate-1/2 scale-[calc(1-0.1*var(--nested-dialogs))] flex-col gap-1 rounded-lg bg-gray-50 p-4 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[top,scale,opacity] duration-100 ease-out after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:bg-black/5 after:opacity-0 after:transition-opacity after:duration-100 after:ease-out data-ending-style:top-[calc(50%+0.25rem+1.25rem*var(--nested-dialogs))] data-ending-style:scale-[0.96] data-ending-style:opacity-0 data-nested-dialog-open:after:opacity-100 data-starting-style:top-[calc(50%+0.25rem+1.25rem*var(--nested-dialogs))] data-starting-style:scale-[0.96] data-starting-style:opacity-0"
        >
          <AlertDialog.Title class="mb-1 text-base font-semibold">Discard note?</AlertDialog.Title>
          <AlertDialog.Description class="mb-4 text-sm text-gray-600">
            Your draft will be lost.
          </AlertDialog.Description>
          <div class="flex items-center justify-end gap-3">
            <AlertDialog.Close
              class="flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 text-sm font-normal text-gray-900 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
            >
              Go back
            </AlertDialog.Close>
            <button
              type="button"
              class="flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 text-sm font-normal text-gray-900 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
              @click="
                () => {
                  confirmationOpen = false
                  dialogOpen = false
                }
              "
            >
              Discard
            </button>
          </div>
        </AlertDialog.Popup>
      </AlertDialog.Portal>
    </AlertDialog.Root>
  </Dialog.Root>
</template>

Detached triggers

<AlertDialog.Trigger> normally sits inside the root. When the trigger and the alert dialog's content can't share a spot in the markup, render <AlertDialog.Trigger> wherever it fits and connect it to the root with a shared handle from AlertDialog.createHandle().

The handle's imperative methods — open(), openWithPayload() and close() — only take effect while an <AlertDialog.Root> using the same handle is mounted. Calls made before a root mounts or after it unmounts are ignored, not queued: each mount starts from fresh state.

<!-- [!code word::handle="h"] -->
<script setup>
const h = AlertDialog.createHandle()
</script>

<template>
  <AlertDialog.Trigger :handle="h">Open</AlertDialog.Trigger>

  <AlertDialog.Root :handle="h">...</AlertDialog.Root>
</template>
<script setup lang="ts">
import { AlertDialog } from '@shardsui/vue/alert-dialog'

const deleteFile = AlertDialog.createHandle()
</script>

<template>
  <AlertDialog.Trigger
    class="flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 text-sm font-normal text-gray-900 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
    :handle="deleteFile"
  >
    Delete
  </AlertDialog.Trigger>

  <AlertDialog.Root :handle="deleteFile">
    <AlertDialog.Portal>
      <AlertDialog.Backdrop
        class="fixed inset-0 min-h-dvh bg-black opacity-20 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-[-webkit-touch-callout:none]:absolute"
      />
      <AlertDialog.Popup
        class="fixed top-1/2 left-1/2 -mt-8 w-96 max-w-[calc(100vw-3rem)] -translate-1/2 rounded-lg bg-gray-50 p-4 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[scale,opacity] duration-100 ease-out data-ending-style:scale-[0.98] data-ending-style:opacity-0 data-starting-style:scale-[0.98] data-starting-style:opacity-0"
      >
        <AlertDialog.Title class="mb-1 text-base font-semibold">Delete file?</AlertDialog.Title>
        <AlertDialog.Description class="mb-4 text-sm text-gray-600">
          This file will be permanently deleted.
        </AlertDialog.Description>
        <div class="flex justify-end gap-3">
          <AlertDialog.Close
            class="flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 text-sm font-normal text-gray-900 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
          >
            Cancel
          </AlertDialog.Close>
          <AlertDialog.Close
            class="flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 text-sm font-normal text-red-800 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
          >
            Delete
          </AlertDialog.Close>
        </div>
      </AlertDialog.Popup>
    </AlertDialog.Portal>
  </AlertDialog.Root>
</template>

Multiple triggers

Several triggers can open the same alert dialog. Share one handle across detached triggers, or drop multiple <AlertDialog.Trigger> components inside a single <AlertDialog.Root>.

<template>
  <AlertDialog.Root>
    <AlertDialog.Trigger>Trigger 1</AlertDialog.Trigger>
    <AlertDialog.Trigger>Trigger 2</AlertDialog.Trigger>
    ...
  </AlertDialog.Root>
</template>
<script setup>
const h = AlertDialog.createHandle()
</script>

<template>
  <AlertDialog.Trigger :handle="h">Trigger 1</AlertDialog.Trigger>
  <AlertDialog.Trigger :handle="h">Trigger 2</AlertDialog.Trigger>
  <AlertDialog.Root :handle="h">...</AlertDialog.Root>
</template>

To show different content depending on which trigger opened the alert dialog, pass a payload to each <AlertDialog.Trigger> and read it through the default slot on <AlertDialog.Root>. Give AlertDialog.createHandle() a type argument to type the payload:

<script setup lang="ts">
const h = AlertDialog.createHandle<{ message: string }>()
</script>

<template>
  <AlertDialog.Trigger :handle="h" :payload="{ message: 'Trigger 1' }"
    >Trigger 1</AlertDialog.Trigger
  >

  <AlertDialog.Trigger :handle="h" :payload="{ message: 'Trigger 2' }"
    >Trigger 2</AlertDialog.Trigger
  >

  <AlertDialog.Root v-slot="{ payload }" :handle="h">
    <AlertDialog.Portal>
      <AlertDialog.Popup>
        <AlertDialog.Title>Alert dialog</AlertDialog.Title>
        <AlertDialog.Description v-if="payload !== undefined">
          Confirming {{ payload.message }}
        </AlertDialog.Description>
      </AlertDialog.Popup>
    </AlertDialog.Portal>
  </AlertDialog.Root>
</template>

Controlled mode with multiple triggers

When the alert dialog's visibility depends on your app's state, drive it with v-model:open on <AlertDialog.Root>. With multiple triggers, give each <AlertDialog.Trigger> an id and add v-model:trigger-id to <AlertDialog.Root>: each trigger publishes its own id when it opens the dialog, and setting triggerId yourself associates the dialog with that trigger.

<script setup lang="ts">
import { AlertDialog } from '@shardsui/vue/alert-dialog'
import { shallowRef } from 'vue'

const confirm = AlertDialog.createHandle<{ title: string; body: string }>()

const open = shallowRef(false)
const triggerId = shallowRef<string | null>(null)

const actions = [
  {
    id: 'discard-draft',
    label: 'Discard draft',
    payload: {
      title: 'Discard draft?',
      body: "This can't be undone."
    }
  },
  {
    id: 'delete-account',
    label: 'Delete account',
    payload: {
      title: 'Delete account?',
      body: 'Your profile and data will be permanently removed.'
    }
  }
]
</script>

<template>
  <div class="flex flex-wrap justify-center gap-2">
    <AlertDialog.Trigger
      v-for="action in actions"
      :key="action.id"
      class="flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 text-sm font-normal text-red-800 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
      :handle="confirm"
      :id="action.id"
      :payload="action.payload"
    >
      {{ action.label }}
    </AlertDialog.Trigger>

    <button
      type="button"
      class="flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 text-sm font-normal text-gray-900 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
      @click="
        () => {
          triggerId = 'delete-account'
          open = true
        }
      "
    >
      Open programmatically
    </button>
  </div>

  <AlertDialog.Root
    v-slot="{ payload }"
    v-model:open="open"
    v-model:trigger-id="triggerId"
    :handle="confirm"
    @update:open="(isOpen) => !isOpen && (triggerId = null)"
  >
    <AlertDialog.Portal>
      <AlertDialog.Backdrop
        class="fixed inset-0 min-h-dvh bg-black opacity-20 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-[-webkit-touch-callout:none]:absolute"
      />
      <AlertDialog.Popup
        class="fixed top-1/2 left-1/2 -mt-8 w-96 max-w-[calc(100vw-3rem)] -translate-1/2 rounded-lg bg-gray-50 p-4 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[scale,opacity] duration-100 ease-out data-ending-style:scale-[0.98] data-ending-style:opacity-0 data-starting-style:scale-[0.98] data-starting-style:opacity-0"
      >
        <AlertDialog.Title class="mb-1 text-base font-semibold">
          {{ payload?.title ?? 'Are you sure?' }}
        </AlertDialog.Title>
        <AlertDialog.Description class="mb-4 text-sm text-gray-600">
          {{ payload?.body ?? 'This action cannot be undone.' }}
        </AlertDialog.Description>
        <div class="flex justify-end gap-3">
          <AlertDialog.Close
            class="flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 text-sm font-normal text-gray-900 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
          >
            Cancel
          </AlertDialog.Close>
          <AlertDialog.Close
            class="flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 text-sm font-normal text-red-800 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
          >
            Confirm
          </AlertDialog.Close>
        </div>
      </AlertDialog.Popup>
    </AlertDialog.Portal>
  </AlertDialog.Root>
</template>

API reference

Root

Groups all parts of the alert dialog. Doesn't render its own HTML element.

PropTypeDefault

Trigger

A button that opens the alert dialog. Renders a <button> element.

PropTypeDefault
AttributeDescription
data-popup-openPresent while the alert dialog is open from this trigger.
data-disabledPresent when the trigger is disabled.

Portal

A portal that moves the popup out to <body>, clear of ancestor clipping and stacking. Renders a <div> element.

PropTypeDefault

Backdrop

An overlay displayed beneath the popup. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-openPresent when the alert dialog is open.
data-closedPresent when the alert dialog is closed.
data-nestedPresent when the alert dialog is nested within another dialog.
data-nested-dialog-openPresent when the alert dialog has other open dialogs nested within.
data-starting-stylePresent when the backdrop is animating in.
data-ending-stylePresent when the backdrop is animating out.

Viewport

A positioning container for the dialog popup that can be made scrollable. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-openPresent when the alert dialog is open.
data-closedPresent when the alert dialog is closed.
data-starting-stylePresent when the alert dialog is animating in.
data-ending-stylePresent when the alert dialog is animating out.
data-nestedPresent when the alert dialog is nested within another dialog.
data-nested-dialog-openPresent when the alert dialog has other open dialogs nested within.

Popup

A container for the dialog contents. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-openPresent when the alert dialog is open.
data-closedPresent when the alert dialog is closed.
data-starting-stylePresent when the alert dialog is animating in.
data-ending-stylePresent when the alert dialog is animating out.
data-nestedPresent when the alert dialog is nested within another dialog.
data-nested-dialog-openPresent when the alert dialog has other open dialogs nested within.
CSS VariableDescription
--nested-dialogsNumber of nested dialogs currently open.

Title

A heading that labels the alert dialog. Renders an <h2> element.

PropTypeDefault

Description

A paragraph with additional information about the alert dialog. Renders a <p> element.

PropTypeDefault

Close

A button that closes the dialog. Renders a <button> element.

PropTypeDefault
AttributeDescription
data-disabledPresent when the button is disabled.

Handle

Connects an <AlertDialog.Root> with detached <AlertDialog.Trigger> components, and controls the alert dialog imperatively. Pass a type argument to type the payload.

const alertDialog = AlertDialog.createHandle<Payload>()
MemberType