HextaUI

Dialog

Una ventana sobre la página para formularios y tareas concretas, con encabezado y pie fijados, anidamiento y una hoja inferior deslizable en móviles.

import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

export function DialogDemo() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="outline" />}>
        Edit profile
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Edit profile</DialogTitle>
          <DialogDescription>
            Make changes to your profile here. Click save when you’re done.
          </DialogDescription>
        </DialogHeader>
        <DialogBody>
          <FieldGroup>
            <Field>
              <FieldLabel>Name</FieldLabel>
              <Input defaultValue="Olivia Martin" autoComplete="name" />
            </Field>
            <Field>
              <FieldLabel>Username</FieldLabel>
              <Input defaultValue="@olivia" autoComplete="username" />
            </Field>
          </FieldGroup>
        </DialogBody>
        <DialogFooter>
          <DialogClose render={<Button variant="outline" />}>
            Cancel
          </DialogClose>
          <DialogClose render={<Button />}>Save changes</DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/dialog.json

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

import {
  Dialog,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
<Dialog>
  <DialogTrigger render={<Button variant="outline" />}>Open</DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Edit profile</DialogTitle>
      <DialogDescription>Make changes to your profile.</DialogDescription>
    </DialogHeader>
    <DialogBody>{/* fields */}</DialogBody>
    <DialogFooter>
      <DialogClose render={<Button variant="outline" />}>Cancel</DialogClose>
      <Button>Save changes</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>
Dialog
├── DialogTrigger
└── DialogContent
    ├── DialogHeader
    │   ├── DialogTitle
    │   └── DialogDescription
    ├── DialogBody
    └── DialogFooter
        └── DialogClose

Form

Mantén el formulario en <DialogBody /> y apunta el botón de envío del pie a él con form. Enter envía, y el diálogo se cierra mediante onOpenChange una vez guardado el valor.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

export function DialogForm() {
  const [open, setOpen] = React.useState(false)
  const [name, setName] = React.useState("Acme")
  const [draft, setDraft] = React.useState(name)
  const formId = React.useId()

  return (
    <div className="flex flex-col items-center gap-3">
      <Dialog
        open={open}
        onOpenChange={(nextOpen) => {
          if (nextOpen) {
            setDraft(name)
          }
          setOpen(nextOpen)
        }}
      >
        <DialogTrigger render={<Button variant="outline" />}>
          Rename workspace
        </DialogTrigger>
        <DialogContent size="sm">
          <DialogHeader>
            <DialogTitle>Rename workspace</DialogTitle>
            <DialogDescription>
              Everyone in the workspace will see the new name.
            </DialogDescription>
          </DialogHeader>
          <DialogBody>
            <form
              id={formId}
              onSubmit={(event) => {
                event.preventDefault()
                setName(draft.trim() || name)
                setOpen(false)
              }}
            >
              <FieldGroup>
                <Field>
                  <FieldLabel>Workspace name</FieldLabel>
                  <Input
                    name="name"
                    value={draft}
                    onChange={(event) => setDraft(event.target.value)}
                    required
                  />
                </Field>
              </FieldGroup>
            </form>
          </DialogBody>
          <DialogFooter>
            <DialogClose render={<Button variant="outline" />}>
              Cancel
            </DialogClose>
            <Button type="submit" form={formId}>
              Save
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
      <p className="text-sm text-muted-foreground">Workspace: {name}</p>
    </div>
  )
}

Botón de cierre personalizado

Oculta el botón de la esquina con showCloseButton={false} en el contenido y añade un botón Close al pie con showCloseButton.

import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogBody,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Field, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

export function DialogCustomCloseButton() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="outline" />}>Share</DialogTrigger>
      <DialogContent showCloseButton={false}>
        <DialogHeader>
          <DialogTitle>Share link</DialogTitle>
          <DialogDescription>
            Anyone who has this link will be able to view this.
          </DialogDescription>
        </DialogHeader>
        <DialogBody>
          <Field>
            <FieldLabel>Link</FieldLabel>
            <Input
              defaultValue="https://hextaui.com/docs/installation"
              readOnly
            />
          </Field>
        </DialogBody>
        <DialogFooter showCloseButton />
      </DialogContent>
    </Dialog>
  )
}

Sin botón de cierre

Sin botón, el diálogo sigue cerrándose con Esc, un clic fuera o un deslizamiento hacia abajo en teléfonos.

import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"

export function DialogNoCloseButton() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="outline" />}>
        No close button
      </DialogTrigger>
      <DialogContent showCloseButton={false}>
        <DialogHeader>
          <DialogTitle>No close button</DialogTitle>
          <DialogDescription>
            Close it with Esc, by clicking outside, or by swiping down on a
            phone.
          </DialogDescription>
        </DialogHeader>
      </DialogContent>
    </Dialog>
  )
}

Tamaños

size define el ancho máximo en pantallas más grandes: sm, default o lg.

import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"

const sizes = [
  { size: "sm", label: "Small" },
  { size: "default", label: "Default" },
  { size: "lg", label: "Large" },
] as const

export function DialogSizes() {
  return (
    <div className="flex flex-wrap justify-center gap-2">
      {sizes.map(({ size, label }) => (
        <Dialog key={size}>
          <DialogTrigger render={<Button variant="outline" />}>
            {label}
          </DialogTrigger>
          <DialogContent size={size}>
            <DialogHeader>
              <DialogTitle>{label} dialog</DialogTitle>
              <DialogDescription>
                On phones every size becomes a full-width bottom sheet.
              </DialogDescription>
            </DialogHeader>
            <DialogFooter>
              <DialogClose render={<Button />}>Done</DialogClose>
            </DialogFooter>
          </DialogContent>
        </Dialog>
      ))}
    </div>
  )
}

El contenido largo en <DialogBody /> se desplaza mientras el encabezado y el pie permanecen en su sitio, así que las acciones siempre están al alcance.

import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"

export function DialogStickyFooter() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="outline" />}>
        Terms of service
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Terms of service</DialogTitle>
          <DialogDescription>Last updated October 2026.</DialogDescription>
        </DialogHeader>
        <DialogBody>
          <div className="flex flex-col gap-3 text-sm text-muted-foreground">
            {Array.from({ length: 20 }, (_, index) => (
              <p key={index}>
                {index + 1}. By using the service you agree to keep your account
                secure and to use it in line with these terms and any laws that
                apply to you.
              </p>
            ))}
          </div>
        </DialogBody>
        <DialogFooter>
          <DialogClose render={<Button variant="outline" />}>
            Decline
          </DialogClose>
          <DialogClose render={<Button />}>Accept</DialogClose>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

Contenido desplazable

Sin pie, el cuerpo se desplaza bajo el encabezado y conserva su relleno inferior.

import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogBody,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"

export function DialogScrollableContent() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="outline" />}>
        Release notes
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>What’s new</DialogTitle>
          <DialogDescription>Version 2.4.0</DialogDescription>
        </DialogHeader>
        <DialogBody>
          <div className="flex flex-col gap-3 text-sm text-muted-foreground">
            {Array.from({ length: 24 }, (_, index) => (
              <p key={index}>
                {index + 1}. Improved performance of the dashboard charts and
                fixed an issue where filters reset after navigation.
              </p>
            ))}
          </div>
        </DialogBody>
      </DialogContent>
    </Dialog>
  )
}

Controlado

Pasa open y onOpenChange para abrirlo desde código, sin un trigger.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog"

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

  return (
    <div className="flex flex-wrap items-center gap-3">
      <Button variant="outline" onClick={() => setOpen(true)}>
        Open from state
      </Button>
      <span className="text-sm text-muted-foreground">
        {open ? "Open" : "Closed"}
      </span>
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Controlled dialog</DialogTitle>
            <DialogDescription>
              The open state lives in your component. Focus returns to the
              button that opened it.
            </DialogDescription>
          </DialogHeader>
          <DialogFooter>
            <DialogClose render={<Button />}>Done</DialogClose>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </div>
  )
}

Anidado

Un dialog o alert dialog abierto desde dentro de otro se apila encima. El padre retrocede y un fondo más claro lo cubre. Esc cierra solo el de arriba.

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"

export function DialogNested() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="outline" />}>
        Workspace settings
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>Workspace settings</DialogTitle>
          <DialogDescription>
            Manage members and the danger zone.
          </DialogDescription>
        </DialogHeader>
        <DialogBody>
          <div className="flex flex-wrap gap-2">
            <Dialog>
              <DialogTrigger render={<Button variant="outline" />}>
                Invite member
              </DialogTrigger>
              <DialogContent size="sm">
                <DialogHeader>
                  <DialogTitle>Invite member</DialogTitle>
                  <DialogDescription>
                    The settings dialog steps back while this one is open.
                  </DialogDescription>
                </DialogHeader>
                <DialogFooter>
                  <DialogClose render={<Button />}>Send invite</DialogClose>
                </DialogFooter>
              </DialogContent>
            </Dialog>
            <AlertDialog>
              <AlertDialogTrigger render={<Button variant="destructive" />}>
                Delete workspace
              </AlertDialogTrigger>
              <AlertDialogContent>
                <AlertDialogHeader>
                  <AlertDialogTitle>Delete workspace?</AlertDialogTitle>
                  <AlertDialogDescription>
                    All projects in this workspace will be removed.
                  </AlertDialogDescription>
                </AlertDialogHeader>
                <AlertDialogFooter>
                  <AlertDialogCancel>Cancel</AlertDialogCancel>
                  <AlertDialogAction variant="destructive">
                    Delete
                  </AlertDialogAction>
                </AlertDialogFooter>
              </AlertDialogContent>
            </AlertDialog>
          </div>
        </DialogBody>
      </DialogContent>
    </Dialog>
  )
}

Triggers desacoplados

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

"use client"

import { Button } from "@/components/ui/button"
import {
  createDialogHandle,
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"

const people = createDialogHandle<{ name: string; role: string }>()

export function DialogDetachedTriggers() {
  return (
    <div className="flex flex-wrap justify-center gap-2">
      <DialogTrigger
        handle={people}
        payload={{ name: "Ada Lovelace", role: "Owner" }}
        render={<Button variant="outline" size="sm" />}
      >
        Ada
      </DialogTrigger>
      <DialogTrigger
        handle={people}
        payload={{ name: "Linus Torvalds", role: "Member" }}
        render={<Button variant="outline" size="sm" />}
      >
        Linus
      </DialogTrigger>
      <Dialog handle={people}>
        {({ payload }) => (
          <DialogContent size="sm">
            <DialogHeader>
              <DialogTitle>{payload?.name}</DialogTitle>
              <DialogDescription>Role: {payload?.role}</DialogDescription>
            </DialogHeader>
            <DialogFooter>
              <DialogClose render={<Button />}>Done</DialogClose>
            </DialogFooter>
          </DialogContent>
        )}
      </Dialog>
    </div>
  )
}

De derecha a izquierda

Pasa también dir al contenido, porque se renderiza en un portal fuera de tu contenedor RTL.

import { Button } from "@/components/ui/button"
import {
  Dialog,
  DialogBody,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

export function DialogRtl() {
  return (
    <div dir="rtl">
      <Dialog>
        <DialogTrigger render={<Button variant="outline" />}>
          تعديل الملف الشخصي
        </DialogTrigger>
        <DialogContent dir="rtl">
          <DialogHeader>
            <DialogTitle>تعديل الملف الشخصي</DialogTitle>
            <DialogDescription>
              قم بإجراء تغييرات على ملفك الشخصي هنا. انقر على حفظ عند الانتهاء.
            </DialogDescription>
          </DialogHeader>
          <DialogBody>
            <FieldGroup>
              <Field>
                <FieldLabel>الاسم</FieldLabel>
                <Input defaultValue="ليلى أحمد" />
              </Field>
              <Field>
                <FieldLabel>اسم المستخدم</FieldLabel>
                <Input defaultValue="@layla" dir="ltr" />
              </Field>
            </FieldGroup>
          </DialogBody>
          <DialogFooter>
            <DialogClose render={<Button variant="outline" />}>
              إلغاء
            </DialogClose>
            <DialogClose render={<Button />}>حفظ التغييرات</DialogClose>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </div>
  )
}
KeyAcción
EnterSpaceEn el trigger, abre el diálogo y mueve el foco a su primer control.
TabShift+TabMueve el foco entre los controles. El foco permanece dentro.
EscCierra el diálogo superior y devuelve el foco a su trigger.
  • El contenido tiene role="dialog", está etiquetado por su título y descrito por su descripción. Incluye siempre un <DialogTitle />.
  • Con ratón o teclado, el foco empieza en el primer control. Con táctil empieza en el propio diálogo, para que el teclado en pantalla no cubra el contenido antes de que se elija un campo. Pasa initialFocus para cambiarlo.
  • El botón de cierre de la esquina tiene la etiqueta “Close”, y la página de detrás es inerte y no se puede desplazar.
  • En teléfonos se convierte en una hoja inferior que se puede deslizar hacia abajo para cerrar. Con movimiento reducido, se desvanece en lugar de escalar o deslizarse.

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
onOpenChange
(open: boolean, details) => void–
onOpenChangeCompleteSe llama después de la animación de apertura o cierre.
(open: boolean) => void–
modal
boolean | "trap-focus"true
disablePointerDismissalMantiene el diálogo abierto al hacer clic fuera.
booleanfalse
handleConecta triggers desacoplados.
DialogHandle<Payload>–
actionsRefCierra o desmonta el diálogo de forma imperativa.
RefObject<{ close, unmount }>–
children
ReactNode | ({ payload }) => ReactNode–
PropTipoPredeterminado
handle
DialogHandle<Payload>–
payloadSe pasa a la función de renderizado del diálogo.
Payload–
render
ReactElement | (props, state) => ReactElement<button>
AtributoDescripción
data-slot="dialog-trigger"Apunta a los triggers en CSS.
data-popup-openPresente mientras su diálogo está abierto.
PropTipoPredeterminado
size
"sm" | "default" | "lg""default"
showCloseButtonMuestra el botón de cierre en la esquina.
booleantrue
initialFocus
boolean | RefObject | (openType) => HTMLElement | booleanFirst control, or the dialog on touch
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="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 título y la descripción. Deja espacio para el botón de cierre.

AtributoDescripción
data-slot="dialog-header"Apunta al encabezado en CSS.

Un <div> que se desplaza cuando el contenido es más alto que la pantalla, manteniendo el encabezado y el pie en su sitio.

AtributoDescripción
data-slot="dialog-body"Selecciona el cuerpo en CSS.

Un <div> para las acciones. Los botones se apilan a todo el ancho en teléfonos, con el primero abajo.

PropTipoPredeterminado
showCloseButtonAñade un botón Close outline después de los hijos.
booleanfalse
AtributoDescripción
data-slot="dialog-footer"Selecciona el pie en CSS.
PropTipoPredeterminado
render
ReactElement | (props, state) => ReactElement<h2>
AtributoDescripción
data-slot="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="dialog-description"Apunta a la descripción en CSS.
PropTipoPredeterminado
render
ReactElement | (props, state) => ReactElement<button>
AtributoDescripción
data-slot="dialog-close"Selecciona los botones de cierre en CSS.

<DialogContent /> 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 <DialogTrigger /> de cualquier parte de la página con un único <Dialog />. Tipa el payload con un genérico: createDialogHandle<{ name: string }>().

Usado en bloques

Bloques que se construyen sobre Dialog.