HextaUI

Alert dialog

Ein Bestätigungsdialog für destruktive oder wichtige Aktionen, der auf asynchrone Arbeit wartet und auf Smartphones zum Bottom Sheet wird.

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

export function AlertDialogDemo() {
  return (
    <AlertDialog>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        Show dialog
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>Publish changes?</AlertDialogTitle>
          <AlertDialogDescription>
            Your changes will be visible to everyone with the link.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialogAction>Publish</AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/alert-dialog.json

Fügt die Komponente, die HextaUI-Theme-Tokens und alle HextaUI-Komponenten hinzu, von denen sie abhängt.

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
<AlertDialog>
  <AlertDialogTrigger render={<Button variant="outline" />}>
    Delete
  </AlertDialogTrigger>
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogTitle>Delete this project?</AlertDialogTitle>
      <AlertDialogDescription>This can’t be undone.</AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel>Cancel</AlertDialogCancel>
      <AlertDialogAction>Delete</AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>
AlertDialog
├── AlertDialogTrigger
└── AlertDialogContent
    ├── AlertDialogHeader
    │   ├── AlertDialogMedia
    │   ├── AlertDialogTitle
    │   └── AlertDialogDescription
    └── AlertDialogFooter
        ├── AlertDialogCancel
        └── AlertDialogAction

Asynchrone Aktion

Gib aus dem onClick von <AlertDialogAction /> ein Promise zurück. Der Button zeigt einen Spinner, und der Dialog lässt sich erst schließen, wenn das Promise abgeschlossen ist. Er schließt sich bei Erfolg und bleibt bei einer Ablehnung offen, damit man es erneut versuchen kann.

"use client"

import * as React from "react"
import { IconTrash } from "@tabler/icons-react"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

function wait(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

export function AlertDialogAsyncAction() {
  const [shouldFail, setShouldFail] = React.useState(false)
  const [attempts, setAttempts] = React.useState(0)

  return (
    <div className="flex flex-wrap items-center gap-2">
      <AlertDialog onOpenChange={(open) => open && setAttempts(0)}>
        <AlertDialogTrigger render={<Button variant="destructive" />}>
          Delete project
        </AlertDialogTrigger>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogMedia variant="destructive">
              <IconTrash />
            </AlertDialogMedia>
            <AlertDialogTitle>Delete “Acme website”?</AlertDialogTitle>
            <AlertDialogDescription>
              {attempts > 0
                ? `Deleting failed ${attempts} time${attempts > 1 ? "s" : ""}. Try again.`
                : "All deployments, domains and environment variables will be removed. This can’t be undone."}
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Cancel</AlertDialogCancel>
            <AlertDialogAction
              variant="destructive"
              onClick={async () => {
                await wait(1500)
                if (shouldFail) {
                  setAttempts((count) => count + 1)
                  throw new Error("Request failed")
                }
              }}
            >
              Delete project
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
      <Button
        variant="ghost"
        size="sm"
        aria-pressed={shouldFail}
        onClick={() => setShouldFail(!shouldFail)}
      >
        Request will {shouldFail ? "fail" : "succeed"}
      </Button>
    </div>
  )
}

Media

<AlertDialogMedia /> platziert ein Icon auf dem Desktop neben dem Titel und auf Smartphones darüber. Verwende variant="destructive" für destruktive Aktionen.

import { IconTrash } from "@tabler/icons-react"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

export function AlertDialogWithMedia() {
  return (
    <AlertDialog>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        Delete file
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogMedia variant="destructive">
            <IconTrash />
          </AlertDialogMedia>
          <AlertDialogTitle>Delete “invoice-2026.pdf”?</AlertDialogTitle>
          <AlertDialogDescription>
            The file moves to the trash and is removed after 30 days.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialogAction variant="destructive">Delete</AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}

Klein

size="sm" zentriert den Inhalt und ordnet die Buttons nebeneinander an, für kurze Fragen.

import { IconLogout } from "@tabler/icons-react"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

export function AlertDialogSmall() {
  return (
    <AlertDialog>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        Sign out
      </AlertDialogTrigger>
      <AlertDialogContent size="sm">
        <AlertDialogHeader>
          <AlertDialogMedia>
            <IconLogout />
          </AlertDialogMedia>
          <AlertDialogTitle>Sign out?</AlertDialogTitle>
          <AlertDialogDescription>
            You’ll need to sign in again on this device.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialogAction>Sign out</AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}

Losgelöste Trigger

Erstelle ein Handle mit createAlertDialogHandle(), um einen Dialog für viele Trigger zu teilen. Jeder Trigger übergibt sein eigenes payload, das der Dialog über eine Render-Funktion liest.

"use client"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
  createAlertDialogHandle,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

const removeMember = createAlertDialogHandle<{ name: string }>()

export function AlertDialogDetachedTriggers() {
  return (
    <div className="flex flex-wrap gap-2">
      {["Olivia", "Liam"].map((name) => (
        <AlertDialogTrigger
          key={name}
          handle={removeMember}
          payload={{ name }}
          render={<Button variant="outline" />}
        >
          Remove {name}
        </AlertDialogTrigger>
      ))}
      <AlertDialog handle={removeMember}>
        {({ payload }) => (
          <AlertDialogContent size="sm">
            <AlertDialogHeader>
              <AlertDialogTitle>Remove {payload?.name}?</AlertDialogTitle>
              <AlertDialogDescription>
                They’ll lose access to this workspace.
              </AlertDialogDescription>
            </AlertDialogHeader>
            <AlertDialogFooter>
              <AlertDialogCancel>Cancel</AlertDialogCancel>
              <AlertDialogAction variant="destructive">
                Remove
              </AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        )}
      </AlertDialog>
    </div>
  )
}

Kontrolliert

Übergib open und onOpenChange, um ihn ohne Trigger aus dem Code zu öffnen. Der Fokus landet trotzdem auf Cancel.

"use client"

import * as React from "react"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

export function AlertDialogControlled() {
  const [open, setOpen] = React.useState(false)

  return (
    <>
      <Button
        variant="outline"
        onClick={() => window.setTimeout(() => setOpen(true), 600)}
      >
        Open in 600ms
      </Button>
      <AlertDialog open={open} onOpenChange={setOpen}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Session expiring</AlertDialogTitle>
            <AlertDialogDescription>
              You’ve been inactive for a while. Stay signed in?
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Sign out</AlertDialogCancel>
            <AlertDialogAction>Stay signed in</AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </>
  )
}

Verschachtelt

Ein Dialog, der aus einem anderen heraus geöffnet wird, legt sich darüber. Der übergeordnete skaliert zurück, solange der untergeordnete offen ist, und kommt beim Schließen wieder nach vorn.

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

export function AlertDialogNested() {
  return (
    <AlertDialog>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        Reset settings
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>Reset all settings?</AlertDialogTitle>
          <AlertDialogDescription>
            Theme, notifications and shortcuts go back to their defaults.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialog>
            <AlertDialogTrigger render={<Button variant="destructive" />}>
              Reset
            </AlertDialogTrigger>
            <AlertDialogContent size="sm">
              <AlertDialogHeader>
                <AlertDialogTitle>Are you really sure?</AlertDialogTitle>
                <AlertDialogDescription>
                  The parent dialog steps back while this one is open.
                </AlertDialogDescription>
              </AlertDialogHeader>
              <AlertDialogFooter>
                <AlertDialogCancel>Go back</AlertDialogCancel>
                <AlertDialogAction variant="destructive">
                  Reset
                </AlertDialogAction>
              </AlertDialogFooter>
            </AlertDialogContent>
          </AlertDialog>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}

Langer Inhalt

Inhalt, der höher als der Bildschirm ist, scrollt im Dialog, während die Seite gesperrt bleibt.

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

const sections = Array.from({ length: 12 }, (_, index) => index + 1)

export function AlertDialogLongContent() {
  return (
    <AlertDialog>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        Review terms
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>Accept the updated terms?</AlertDialogTitle>
          <AlertDialogDescription render={<div />}>
            {sections.map((section) => (
              <p key={section}>
                Section {section}. We may update these terms from time to time.
                Continued use of the service after changes take effect means you
                accept the new terms. Read the <a href="#">full policy</a> for
                details.
              </p>
            ))}
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Decline</AlertDialogCancel>
          <AlertDialogAction>Accept</AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}

Rechts nach links

Übergib dir auch an den Content, da er in einem Portal außerhalb deines RTL-Containers gerendert wird. Die Pfeiltasten im Footer folgen der Leserichtung.

import { IconTrash } from "@tabler/icons-react"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

export function AlertDialogRtl() {
  return (
    <div dir="rtl">
      <AlertDialog>
        <AlertDialogTrigger render={<Button variant="outline" />}>
          حذف الملف
        </AlertDialogTrigger>
        <AlertDialogContent dir="rtl">
          <AlertDialogHeader>
            <AlertDialogMedia variant="destructive">
              <IconTrash />
            </AlertDialogMedia>
            <AlertDialogTitle>حذف هذا الملف؟</AlertDialogTitle>
            <AlertDialogDescription>
              لا يمكن التراجع عن هذا الإجراء.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>إلغاء</AlertDialogCancel>
            <AlertDialogAction variant="destructive">حذف</AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </div>
  )
}
TasteAktion
EnterSpaceAm Trigger: öffnet den Dialog und setzt den Fokus auf Cancel.
TabShift+TabBewegt den Fokus zwischen den Bedienelementen. Der Fokus bleibt im Dialog.
←→↑↓Wechselt zwischen den Footer-Buttons und springt am Ende wieder an den Anfang. Links und rechts folgen der Leserichtung.
EscSchließt den Dialog und gibt den Fokus an den Trigger zurück. Wird ignoriert, solange eine asynchrone Aktion läuft.
  • Der Content hat role="alertdialog", wird durch seinen Titel beschriftet und durch seine Beschreibung beschrieben.
  • Der Fokus startet auf <AlertDialogCancel />, der am wenigsten destruktiven Option. Übergib initialFocus, um das zu ändern.
  • Ein Klick auf den Backdrop schließt ihn nicht, sodass eine Entscheidung nie versehentlich verworfen wird. Setze disablePointerDismissal={false}, um das zu erlauben.
  • Auf Smartphones wird er zu einem Bottom Sheet, das sich zum Abbrechen nach unten wischen lässt. Solange eine Aktion aussteht, sind Wischen, Esc und Cancel blockiert, und die Buttons bleiben fokussierbar.
  • Bei reduzierter Bewegung wird eingeblendet statt skaliert.

Basiert über <Sheet /> auf dem Base UI Drawer. Jeder Teil akzeptiert die Props der Primitive oder des Elements, das er umschließt.

PropTypStandard
open
boolean–
defaultOpen
booleanfalse
onOpenChangeWird bei Schließvorgängen, die durch eine ausstehende Aktion blockiert sind, nicht aufgerufen.
(open: boolean, details) => void–
onOpenChangeCompleteWird nach der Öffnen- oder Schließen-Animation aufgerufen.
(open: boolean) => void–
disablePointerDismissalHält den Dialog bei Klicks auf den Backdrop offen.
booleantrue
handleVerbindet losgelöste Trigger.
AlertDialogHandle<Payload>–
actionsRefDen Dialog imperativ schließen oder unmounten.
RefObject<{ close, unmount }>–
children
ReactNode | ({ payload }) => ReactNode–
PropTypStandard
handle
AlertDialogHandle<Payload>–
payloadWird an die Render-Funktion des Dialogs übergeben.
Payload–
render
ReactElement | (props, state) => ReactElement<button>
AttributBeschreibung
data-slot="alert-dialog-trigger"Trigger in CSS ansprechen.
data-popup-openVorhanden, solange sein Dialog geöffnet ist.
PropTypStandard
size
"default" | "sm""default"
initialFocus
boolean | RefObject | (openType) => HTMLElement | booleanCancel button
finalFocus
boolean | RefObject | (closeType) => HTMLElement | booleanThe trigger
dirSetze es, wenn der Dialog von rechts nach links laufen soll.
"ltr" | "rtl"–
render
ReactElement | (props, state) => ReactElement<div>
AttributBeschreibung
data-slot="alert-dialog-content"Den Dialog in CSS ansprechen.
data-sizeDie aktuelle Größe.
data-openVorhanden, solange geöffnet.
data-starting-styleVorhanden, während der Dialog eingeblendet wird.
data-ending-styleVorhanden, während der Dialog ausgeblendet wird.
data-nested-drawer-openVorhanden, solange darüber ein verschachtelter Dialog geöffnet ist.
data-swipingVorhanden, solange er auf einem Smartphone gewischt wird.
--nested-drawersWie viele Dialoge über diesem geöffnet sind.

Eine <div>, die Media, Titel und Beschreibung stapelt.

AttributBeschreibung
data-slot="alert-dialog-header"Den Header in CSS ansprechen.
PropTypStandard
variant
"default" | "destructive""default"
AttributBeschreibung
data-slot="alert-dialog-media"Das Media in CSS ansprechen.
PropTypStandard
render
ReactElement | (props, state) => ReactElement<h2>
AttributBeschreibung
data-slot="alert-dialog-title"Den Titel in CSS ansprechen.
PropTypStandard
renderVerwende render={<div />}, wenn er mehrere Absätze enthält.
ReactElement | (props, state) => ReactElement<p>
AttributBeschreibung
data-slot="alert-dialog-description"Die Beschreibung in CSS ansprechen.

Eine <div> für die Buttons. Auf Smartphones werden die Buttons in voller Breite gestapelt, und die Pfeiltasten wechseln zwischen ihnen.

AttributBeschreibung
data-slot="alert-dialog-footer"Den Footer in CSS ansprechen.

Eine <Button />, die den Dialog beim Klicken schließt. Sie akzeptiert alle Button-Props.

PropTypStandard
onClickGib ein Promise zurück, um einen Spinner anzuzeigen und den Dialog bis zu dessen Abschluss offen zu halten. Rufe event.preventDefault() auf, um ihn offen zu halten.
(event) => void | PromiseLike<unknown>–
variant
ButtonVariant"default"
AttributBeschreibung
data-slot="alert-dialog-action"Actions in CSS ansprechen.
PropTypStandard
variant
ButtonVariant"outline"
size
ButtonSize–
disabledEbenfalls deaktiviert, solange eine Aktion aussteht.
boolean–
AttributBeschreibung
data-slot="alert-dialog-cancel"Den Cancel-Button in CSS ansprechen. Er erhält den anfänglichen Fokus.
data-disabledVorhanden, solange deaktiviert.

<AlertDialogContent /> rendert bereits beides. Verwende sie nur, wenn du ein eigenes Popup zusammensetzt.

PropTypStandard
keepMountedHält den Dialog im geschlossenen Zustand im DOM, wenn es am Portal gesetzt ist.
booleanfalse

Gibt ein Handle zurück, das <AlertDialogTrigger />-Elemente an beliebiger Stelle der Seite mit einem <AlertDialog /> verbindet. Typisiere das Payload per Generic: createAlertDialogHandle<{ name: string }>().

In Blocks verwendet

Blocks, die auf Alert dialog aufbauen.