HextaUI

Alert dialog

Un diálogo de confirmación para acciones destructivas o importantes que espera el trabajo asíncrono y se convierte en una hoja inferior en móviles.

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

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que depende.

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

Acción asíncrona

Devuelve una promesa desde el onClick de <AlertDialogAction />. El botón muestra un spinner y el diálogo no se puede cerrar hasta que la promesa se resuelva. Se cierra cuando se cumple y permanece abierto cuando se rechaza, para que se pueda reintentar.

"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 /> coloca un icono junto al título en escritorio y encima en teléfonos. Usa variant="destructive" para acciones destructivas.

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>
  )
}

Pequeño

size="sm" centra el contenido y coloca los botones uno al lado del otro, para preguntas cortas.

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>
  )
}

Triggers desacoplados

Crea un handle con createAlertDialogHandle() para compartir un diálogo entre muchos triggers. Cada trigger pasa su propio payload, que el diálogo lee mediante una función de renderizado.

"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>
  )
}

Controlado

Pasa open y onOpenChange para abrirlo desde código, sin un trigger. El foco sigue yendo a 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>
    </>
  )
}

Anidado

Un diálogo abierto desde dentro de otro se apila encima. El principal se reduce mientras el hijo está abierto y vuelve al frente cuando este se cierra.

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>
  )
}

Contenido largo

El contenido más alto que la pantalla se desplaza dentro del diálogo mientras la página permanece bloqueada.

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>
  )
}

De derecha a izquierda

Pasa también dir al contenido, porque se renderiza en un portal fuera de tu contenedor RTL. Las teclas de flecha en el pie siguen la dirección de lectura.

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>
  )
}
KeyAcción
EnterSpaceEn el trigger, abre el diálogo y mueve el foco a Cancel.
TabShift+TabMueve el foco entre los controles. El foco permanece dentro.
←→↑↓Se mueve entre los botones del pie y da la vuelta al llegar al final. Izquierda y derecha siguen la dirección de lectura.
EscCierra el diálogo y devuelve el foco al trigger. Se ignora mientras una acción asíncrona está en curso.
  • El contenido tiene role="alertdialog", está etiquetado por su título y descrito por su descripción.
  • El foco empieza en <AlertDialogCancel />, la opción menos destructiva. Pasa initialFocus para cambiarlo.
  • Hacer clic en el fondo no lo cierra, así que una decisión nunca se descarta por accidente. Define disablePointerDismissal={false} para permitirlo.
  • En teléfonos se convierte en una hoja inferior que se puede deslizar hacia abajo para cancelar. Mientras una acción está pendiente, deslizar, Esc y Cancel quedan bloqueados y los botones siguen pudiendo recibir foco.
  • Con movimiento reducido, se desvanece en lugar de escalar.

Construido sobre el drawer de Base UI, mediante <Sheet />. Cada parte acepta las props de la primitiva o el elemento que envuelve.

PropTipoPredeterminado
open
boolean–
defaultOpen
booleanfalse
onOpenChangeNo se llama en los cierres bloqueados por una acción pendiente.
(open: boolean, details) => void–
onOpenChangeCompleteSe llama después de la animación de apertura o cierre.
(open: boolean) => void–
disablePointerDismissalMantiene el diálogo abierto al hacer clic en el fondo.
booleantrue
handleConecta triggers desacoplados.
AlertDialogHandle<Payload>–
actionsRefCierra o desmonta el diálogo de forma imperativa.
RefObject<{ close, unmount }>–
children
ReactNode | ({ payload }) => ReactNode–
PropTipoPredeterminado
handle
AlertDialogHandle<Payload>–
payloadSe pasa a la función de renderizado del diálogo.
Payload–
render
ReactElement | (props, state) => ReactElement<button>
AtributoDescripción
data-slot="alert-dialog-trigger"Apunta a los triggers en CSS.
data-popup-openPresente mientras su diálogo está abierto.
PropTipoPredeterminado
size
"default" | "sm""default"
initialFocus
boolean | RefObject | (openType) => HTMLElement | booleanCancel button
finalFocus
boolean | RefObject | (closeType) => HTMLElement | booleanThe trigger
dirDefínelo cuando el diálogo deba ir de derecha a izquierda.
"ltr" | "rtl"–
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescripción
data-slot="alert-dialog-content"Selecciona el diálogo en CSS.
data-sizeEl tamaño actual.
data-openPresente mientras está abierto.
data-starting-stylePresente mientras el diálogo se anima al abrirse.
data-ending-stylePresente mientras el diálogo se anima al cerrarse.
data-nested-drawer-openPresente mientras un diálogo anidado está abierto encima.
data-swipingPresente mientras se desliza en un teléfono.
--nested-drawersCuántos diálogos están abiertos encima de este.

Un <div> que apila el elemento multimedia, el título y la descripción.

AtributoDescripción
data-slot="alert-dialog-header"Apunta al encabezado en CSS.
PropTipoPredeterminado
variant
"default" | "destructive""default"
AtributoDescripción
data-slot="alert-dialog-media"Selecciona el elemento multimedia en CSS.
PropTipoPredeterminado
render
ReactElement | (props, state) => ReactElement<h2>
AtributoDescripción
data-slot="alert-dialog-title"Apunta al título en CSS.
PropTipoPredeterminado
renderUsa render={<div />} cuando contenga varios párrafos.
ReactElement | (props, state) => ReactElement<p>
AtributoDescripción
data-slot="alert-dialog-description"Apunta a la descripción en CSS.

Un <div> para los botones. Los botones se apilan a todo el ancho en teléfonos, y las teclas de flecha se mueven entre ellos.

AtributoDescripción
data-slot="alert-dialog-footer"Selecciona el pie en CSS.

Un <Button /> que cierra el diálogo al hacer clic. Acepta todas las props de Button.

PropTipoPredeterminado
onClickDevuelve una promesa para mostrar un spinner y mantener el diálogo abierto hasta que se resuelva. Llama a event.preventDefault() para mantenerlo abierto.
(event) => void | PromiseLike<unknown>–
variant
ButtonVariant"default"
AtributoDescripción
data-slot="alert-dialog-action"Selecciona las acciones en CSS.
PropTipoPredeterminado
variant
ButtonVariant"outline"
size
ButtonSize–
disabledTambién se deshabilita mientras una acción está pendiente.
boolean–
AtributoDescripción
data-slot="alert-dialog-cancel"Selecciona el botón de cancelar en CSS. Recibe el foco inicial.
data-disabledPresente mientras está deshabilitado.

<AlertDialogContent /> ya renderiza ambos. Úsalos solo al componer un popup personalizado.

PropTipoPredeterminado
keepMountedEn el portal, mantiene el diálogo en el DOM mientras está cerrado.
booleanfalse

Devuelve un handle que conecta elementos <AlertDialogTrigger /> de cualquier parte de la página con un único <AlertDialog />. Tipa el payload con un genérico: createAlertDialogHandle<{ name: string }>().

Usado en bloques

Bloques que se construyen sobre Alert dialog.