HextaUI

Dialog

Ein Fenster über der Seite für Formulare und fokussierte Aufgaben, mit fixiertem Header und Footer, Verschachtelung und einem wischbaren Bottom Sheet auf Smartphones.

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

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

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

Behalte das Formular in <DialogBody /> und verweise den Submit-Button im Footer mit form darauf. Enter sendet ab, und der Dialog schließt sich über onOpenChange, sobald der Wert gespeichert ist.

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

Eigener Schließen-Button

Blende den Eck-Button mit showCloseButton={false} am Content aus und füge dem Footer mit showCloseButton einen Close-Button hinzu.

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

Kein Schließen-Button

Ohne Button schließt sich der Dialog weiterhin mit Esc, einem Klick nach außen oder auf Smartphones mit einem Wischen nach unten.

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

Größen

size legt auf größeren Bildschirmen die maximale Breite fest: sm, default oder 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>
  )
}

Langer Inhalt in <DialogBody /> scrollt, während Header und Footer an Ort und Stelle bleiben, sodass die Aktionen immer in Reichweite sind.

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

Scrollbarer Inhalt

Ohne Footer scrollt der Body unter dem Header und behält sein unteres Padding.

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

Kontrolliert

Übergib open und onOpenChange, um ihn ohne Trigger aus dem Code zu öffnen.

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

Verschachtelt

Ein Dialog oder Alert Dialog, der aus einem anderen heraus geöffnet wird, legt sich darüber. Der untere tritt zurück, und ein hellerer Backdrop deckt ihn ab. Esc schließt nur den obersten.

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

Losgelöste Trigger

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

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

Rechts nach links

Übergib dir auch an den Content, da er in einem Portal außerhalb deines RTL-Containers gerendert wird.

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>
  )
}
TasteAktion
EnterSpaceAm Trigger: öffnet den Dialog und setzt den Fokus auf sein erstes Bedienelement.
TabShift+TabBewegt den Fokus zwischen den Bedienelementen. Der Fokus bleibt im Dialog.
EscSchließt den obersten Dialog und gibt den Fokus an seinen Trigger zurück.
  • Der Content hat role="dialog", wird durch seinen Titel beschriftet und durch seine Beschreibung beschrieben. Binde immer einen <DialogTitle /> ein.
  • Mit Maus oder Tastatur startet der Fokus auf dem ersten Bedienelement. Bei Touch startet er auf dem Dialog selbst, damit die Bildschirmtastatur den Inhalt nicht verdeckt, bevor man ein Feld wählt. Übergib initialFocus, um das zu ändern.
  • Der Schließen-Button in der Ecke ist mit „Close“ beschriftet, und die Seite dahinter ist inert und lässt sich nicht scrollen.
  • Auf Smartphones wird er zu einem Bottom Sheet, das sich zum Schließen nach unten wischen lässt. Bei reduzierter Bewegung blendet er ein und aus, statt zu skalieren oder zu gleiten.

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
onOpenChange
(open: boolean, details) => void–
onOpenChangeCompleteWird nach der Öffnen- oder Schließen-Animation aufgerufen.
(open: boolean) => void–
modal
boolean | "trap-focus"true
disablePointerDismissalHält den Dialog bei Klicks nach außen offen.
booleanfalse
handleVerbindet losgelöste Trigger.
DialogHandle<Payload>–
actionsRefDen Dialog imperativ schließen oder unmounten.
RefObject<{ close, unmount }>–
children
ReactNode | ({ payload }) => ReactNode–
PropTypStandard
handle
DialogHandle<Payload>–
payloadWird an die Render-Funktion des Dialogs übergeben.
Payload–
render
ReactElement | (props, state) => ReactElement<button>
AttributBeschreibung
data-slot="dialog-trigger"Trigger in CSS ansprechen.
data-popup-openVorhanden, solange sein Dialog geöffnet ist.
PropTypStandard
size
"sm" | "default" | "lg""default"
showCloseButtonZeigt den Schließen-Button in der Ecke.
booleantrue
initialFocus
boolean | RefObject | (openType) => HTMLElement | booleanFirst control, or the dialog on touch
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="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 Titel und Beschreibung stapelt. Sie lässt Platz für den Schließen-Button.

AttributBeschreibung
data-slot="dialog-header"Den Header in CSS ansprechen.

Eine <div>, die scrollt, wenn der Inhalt höher als der Bildschirm ist, und Header und Footer an Ort und Stelle hält.

AttributBeschreibung
data-slot="dialog-body"Den Body in CSS ansprechen.

Eine <div> für die Aktionen. Auf Smartphones werden Buttons in voller Breite gestapelt, der erste unten.

PropTypStandard
showCloseButtonFügt nach den Kindern einen Close-Button im Outline-Stil hinzu.
booleanfalse
AttributBeschreibung
data-slot="dialog-footer"Den Footer in CSS ansprechen.
PropTypStandard
render
ReactElement | (props, state) => ReactElement<h2>
AttributBeschreibung
data-slot="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="dialog-description"Die Beschreibung in CSS ansprechen.
PropTypStandard
render
ReactElement | (props, state) => ReactElement<button>
AttributBeschreibung
data-slot="dialog-close"Schließen-Buttons in CSS ansprechen.

<DialogContent /> 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 <DialogTrigger />-Elemente an beliebiger Stelle der Seite mit einem <Dialog /> verbindet. Typisiere das Payload per Generic: createDialogHandle<{ name: string }>().

In Blocks verwendet

Blocks, die auf Dialog aufbauen.