# Dialog

> Focus attention on a short task or decision without leaving the current page.

Source: https://www.honestui.com/docs/components/dialog

```tsx
import { Button } from "@/components/honest-ui/ui/button"
import {
  Dialog,
  DialogBody,
  DialogClose,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogPopup,
  DialogTitle,
  DialogTrigger,
} from "@/components/honest-ui/ui/dialog"
import { Field, FieldControl, FieldLabel } from "@/components/honest-ui/ui/field"
import { Form } from "@/components/honest-ui/ui/form"

export function DialogDemo() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="secondary" />}>
        Open Dialog
      </DialogTrigger>
      <DialogPopup className="sm:max-w-sm">
        <Form className="grid">
          <DialogHeader>
            <DialogTitle>Edit profile</DialogTitle>
            <DialogDescription>
              Make changes to your profile here. Click save when
              you&apos;re done.
            </DialogDescription>
          </DialogHeader>
          <DialogBody className="flex flex-col gap-4">
            <Field>
              <FieldLabel>Name</FieldLabel>
              <FieldControl type="text" defaultValue="Connor Love" />
            </Field>
            <Field>
              <FieldLabel>Username</FieldLabel>
              <FieldControl type="text" defaultValue="@loveconnor" />
            </Field>
          </DialogBody>
          <DialogFooter>
            <DialogClose render={<Button variant="ghost" />}>
              Cancel
            </DialogClose>
            <Button type="submit">Save</Button>
          </DialogFooter>
        </Form>
      </DialogPopup>
    </Dialog>
  )
}

```

## Overview [#overview]

Use a dialog when the person needs to focus on one task without leaving the current page: short forms, confirmations, detail views, focused editing. Avoid dialogs for large multi-step experiences that deserve their own URL and back-button behavior.

## Anatomy [#anatomy]

A dialog has a trigger, popup, header, title, description, content, footer, and close action. The title names the task; the description states its consequence. The footer holds the main action and a way to back out.

## Behavior [#behavior]

**Opening.** When the dialog opens as modal (the default), focus moves into the popup and the page behind it becomes inert. Scrolling behind is locked.

**While open.** <kbd>Tab</kbd> and <kbd>Shift</kbd>+<kbd>Tab</kbd> cycle inside the popup only. <kbd>Escape</kbd> closes it, and clicking the backdrop closes it in non-blocking cases. Use `initialFocus` on the popup to place focus on a specific control instead of the first tabbable one.

**Closing.** Focus returns to the trigger so keyboard users continue where they left off. If the dialog contains unsaved input, intercept close attempts with controlled state and confirm before discarding — see the [close confirmation](#close-confirmation) example.

Nested dialogs are for true interruptions only, such as confirming an exit while a form has unsaved changes.

## Accessibility [#accessibility]

Always provide a `DialogTitle`; it gives the dialog its accessible name, so screen-reader users know what opened. Add `DialogDescription` when the task needs context or states consequences.

Keep destructive actions explicit: name the affected object in the title ("Delete Aurora website?"), describe what is lost, and style the confirming button with the destructive variant. Place initial focus on the safe action, not the destructive one.

The title should survive truncation on small screens; keep it short enough to read at 200% zoom without wrapping past two lines.

Colors and overlays come from theme tokens, so dialogs render correctly in dark mode automatically. The backdrop dims whatever is behind using tokens as well; verify legibility if you layer custom content under a non-modal dialog.

Dialogs mirror correctly in right-to-left locales because positioning uses logical properties; long localized titles wrap rather than overflow the popup.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;dialog&#x22;]" />
  

  
    
      
        Install the following dependencies:
      

      ```bash
      npm install @base-ui-components/react
      ```

      
        Copy and paste the following code into your project.
      

      ### components/ui/dialog.tsx

```tsx
"use client"

import { Dialog as DialogPrimitive } from "@base-ui-components/react/dialog"
import { X as XIcon } from "honestui/icons"

import { cn } from "@/lib/utils"

const Dialog = DialogPrimitive.Root

function DialogTrigger(props: DialogPrimitive.Trigger.Props) {
  return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}

function DialogPortal(props: DialogPrimitive.Portal.Props) {
  return <DialogPrimitive.Portal {...props} />
}

function DialogClose(props: DialogPrimitive.Close.Props) {
  return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}

function DialogBackdrop({
  className,
  ...props
}: DialogPrimitive.Backdrop.Props) {
  return (
    <DialogPrimitive.Backdrop
      data-slot="dialog-backdrop"
      className={cn(
        "fixed inset-0 z-[var(--hui-z-index-portal)] bg-[var(--hui-color-overlay-black-a5)] [backdrop-filter:var(--hui-blur-lg)] [transition:opacity_var(--hui-duration-normal)_var(--hui-ease-out)] data-ending-style:opacity-0 data-starting-style:opacity-0 [@media(prefers-reduced-transparency:reduce)]:bg-[var(--hui-color-overlay-black-a9)] [@media(prefers-reduced-transparency:reduce)]:[backdrop-filter:none]",
        className
      )}
      {...props}
    />
  )
}

function DialogPopup({
  className,
  children,
  showCloseButton = true,
  showNestedAnimation = true,
  ...props
}: DialogPrimitive.Popup.Props & {
  showCloseButton?: boolean
  showNestedAnimation?: boolean
}) {
  return (
    <DialogPortal>
      <DialogBackdrop />
      <div
        data-slot="dialog-viewport"
        className="pointer-events-none fixed inset-0 z-[var(--hui-z-index-portal)] flex items-center justify-center"
      >
        <DialogPrimitive.Popup
          data-slot="dialog-popup"
          className={cn(
            "pointer-events-auto fixed top-0 left-1/2 min-h-[100px] max-h-[85vh] w-[400px] min-w-[200px] max-w-[90vw] overflow-auto rounded-[var(--hui-radius-2)] bg-[var(--hui-color-background-base-primary)] p-0 shadow-[var(--hui-shadow-floating)] [transform:translate(-50%,min(160px,calc(50vh-50%)))] outline-none [transition:opacity_var(--hui-duration-normal)_var(--hui-ease-out)] focus:outline-none data-ending-style:opacity-0 data-starting-style:opacity-0 after:pointer-events-none data-nested-dialog-open:after:absolute data-nested-dialog-open:after:inset-0 data-nested-dialog-open:after:rounded-[inherit] data-nested-dialog-open:after:bg-[var(--hui-color-overlay-black-a1)] motion-safe:[transition:opacity_var(--hui-duration-normal)_var(--hui-ease-out),transform_var(--hui-duration-normal)_var(--hui-ease-out)]",
            showNestedAnimation &&
              "[transform:translate(-50%,min(160px,calc(50vh-50%)))_scale(calc(1-0.1*var(--nested-dialogs,0)))] [translate:0_calc(1.25rem*var(--nested-dialogs,0))] motion-safe:data-ending-style:[transform:translate(-50%,min(160px,calc(50vh-50%)))_scale(0.9)] motion-safe:data-starting-style:[transform:translate(-50%,min(160px,calc(50vh-50%)))_scale(0.9)]",
            className
          )}
          {...props}
        >
          {children}
          {showCloseButton && (
            <DialogPrimitive.Close className="absolute top-[18px] right-[var(--hui-space-7)] inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-[var(--hui-radius-2)] border border-transparent opacity-70 outline-none hover:opacity-100 focus-visible:[outline:var(--hui-focus-ring)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
              <XIcon />
              <span className="sr-only">Close</span>
            </DialogPrimitive.Close>
          )}
        </DialogPrimitive.Popup>
      </div>
    </DialogPortal>
  )
}

function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="dialog-header"
      className={cn(
        "flex flex-col gap-1 border-b border-[var(--hui-color-border-base-primary)] px-[var(--hui-space-7)] py-[var(--hui-space-5)] text-left",
        className
      )}
      {...props}
    />
  )
}

function DialogBody({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="dialog-body"
      className={cn(
        "px-[var(--hui-space-7)] py-[var(--hui-space-9)]",
        className
      )}
      {...props}
    />
  )
}

function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="dialog-footer"
      className={cn(
        "flex flex-col-reverse gap-2 px-[var(--hui-space-7)] py-[var(--hui-space-5)] [[data-slot=dialog-body]+&]:border-t [[data-slot=dialog-body]+&]:border-[var(--hui-color-border-base-primary)] sm:flex-row sm:justify-end",
        className
      )}
      {...props}
    />
  )
}

function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
  return (
    <DialogPrimitive.Title
      data-slot="dialog-title"
      className={cn(
        "m-0 text-[var(--hui-color-foreground-base-primary)] [font-size:var(--hui-font-size-large)] [font-style:normal] [font-weight:var(--hui-font-weight-medium)] [letter-spacing:var(--hui-letter-spacing-large)] [line-height:var(--hui-line-height-large)]",
        className
      )}
      {...props}
    />
  )
}

function DialogDescription({
  className,
  ...props
}: DialogPrimitive.Description.Props) {
  return (
    <DialogPrimitive.Description
      data-slot="dialog-description"
      className={cn(
        "m-0 text-[var(--hui-color-foreground-base-secondary)] [font-size:var(--hui-font-size-small)] [font-style:normal] [font-weight:var(--hui-font-weight-regular)] [letter-spacing:var(--hui-letter-spacing-small)] [line-height:var(--hui-line-height-small)]",
        className
      )}
      {...props}
    />
  )
}

export {
  Dialog,
  DialogTrigger,
  DialogPortal,
  DialogClose,
  DialogBackdrop,
  DialogBackdrop as DialogOverlay,
  DialogPopup,
  DialogPopup as DialogContent,
  DialogHeader,
  DialogBody,
  DialogFooter,
  DialogTitle,
  DialogDescription,
}

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import {
  Dialog,
  DialogBody,
  DialogClose,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogPopup,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
```

```tsx
<Dialog>
  <DialogTrigger>Edit profile</DialogTrigger>
  <DialogPopup>
    <DialogHeader>
      <DialogTitle>Edit profile</DialogTitle>
      <DialogDescription>
        Update the name shown to other workspace members.
      </DialogDescription>
    </DialogHeader>
    <DialogBody>{/* Profile fields */}</DialogBody>
    <DialogFooter>
      <DialogClose>Close</DialogClose>
    </DialogFooter>
  </DialogPopup>
</Dialog>
```

## Don't do this [#dont-do-this]

### Letting Enter trigger destruction [#letting-enter-trigger-destruction]

```tsx
// Bad
<DialogFooter>
  <Button variant="ghost" onClick={close}>Cancel</Button>
  <Button variant="destructive" onClick={destroy}>Delete project</Button>
</DialogFooter>
```

```tsx
// Good
<DialogPopup initialFocus={cancelRef}>
  <DialogFooter>
    <DialogClose render={<Button variant="ghost" />} ref={cancelRef}>
      Cancel
    </DialogClose>
    <Button variant="destructive" onClick={destroy}>Delete project</Button>
  </DialogFooter>
</DialogPopup>
```

Without `initialFocus`, focus lands on the first tabbable element — often the destructive button itself. A person pressing <kbd>Enter</kbd>, or double-clicking out of habit, confirms an irreversible action by accident. Start focus on the safe action and let the destructive one require deliberate movement toward it.

### Using a dialog where a page belongs [#using-a-dialog-where-a-page-belongs]

```tsx
// Bad
<Dialog>
  <DialogTrigger>Settings</DialogTrigger>
  <DialogPopup>
    {/* Six sections of settings */}
  </DialogPopup>
</Dialog>
```

Large surfaces inside a dialog lose everything a page provides: no URL to share or bookmark, browser Back exits the whole flow instead of a step, and reload discards progress. If content scrolls more than a screen or represents navigation to somewhere, give it a route.

### Opening a dialog without a title [#opening-a-dialog-without-a-title]

```tsx
// Bad
<DialogPopup>
  <DialogBody>{/* fields */}</DialogBody>
</DialogPopup>
```

The accessible name comes from the title. Without it, assistive technology announces an unlabeled dialog and people cannot tell what they just opened. Every popup gets a `DialogTitle`, even when it looks minimal.

## Examples [#examples]

### Destructive confirmation [#destructive-confirmation]

Focus starts on Cancel via `initialFocus`; the destructive action requires deliberate activation.

```tsx
"use client";

import * as React from "react";
import { Trash as TrashIcon } from "honestui/icons";

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

export function DialogDestructiveConfirm() {
  const cancelRef = React.useRef<HTMLButtonElement>(null);
  const [open, setOpen] = React.useState(false);
  const [deleted, setDeleted] = React.useState(false);

  return (
    <div className="flex flex-col items-center gap-3">
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogTrigger render={<Button variant="outline" />}>
          <TrashIcon aria-hidden="true" />
          Delete project
        </DialogTrigger>
        <DialogPopup initialFocus={cancelRef} showCloseButton={false}>
          <DialogHeader>
            <DialogTitle>Delete “Aurora website”?</DialogTitle>
            <DialogDescription>
              The project, its deployments, and its environment variables are
              permanently deleted. This cannot be undone.
            </DialogDescription>
          </DialogHeader>
          <DialogFooter>
            <DialogClose
              render={<Button variant="ghost" ref={cancelRef} />}
            >
              Cancel
            </DialogClose>
            <Button
              variant="destructive"
              onClick={() => {
                setDeleted(true);
                setOpen(false);
              }}
            >
              Delete project
            </Button>
          </DialogFooter>
        </DialogPopup>
      </Dialog>
      <p
        aria-live="polite"
        className="min-h-[var(--hui-space-5)] text-[length:var(--hui-font-size-mini)] text-muted-foreground"
      >
        {deleted ? "Project deleted." : null}
      </p>
    </div>
  );
}

```

### Form submission with save pending [#form-submission-with-save-pending]

While the invite sends, both buttons disable and closing is blocked so work cannot be silently discarded.

```tsx
"use client";

import * as React from "react";
import { LoaderCircle as LoaderCircleIcon } from "honestui/icons";

import { Button } from "@/components/honest-ui/ui/button";
import {
  Dialog,
  DialogBody,
  DialogClose,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogPopup,
  DialogTitle,
  DialogTrigger,
} from "@/components/honest-ui/ui/dialog";
import { Field, FieldControl, FieldLabel } from "@/components/honest-ui/ui/field";
import { Form } from "@/components/honest-ui/ui/form";

export function DialogFormSubmit() {
  const [open, setOpen] = React.useState(false);
  const [saving, setSaving] = React.useState(false);

  function handleOpenChange(nextOpen: boolean) {
    if (!nextOpen && saving) {
      return;
    }
    setOpen(nextOpen);
  }

  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setSaving(true);
    setTimeout(() => {
      setSaving(false);
      setOpen(false);
    }, 1200);
  }

  return (
    <Dialog open={open} onOpenChange={handleOpenChange}>
      <DialogTrigger render={<Button variant="secondary" />}>
        Invite member
      </DialogTrigger>
      <DialogPopup>
        <DialogHeader>
          <DialogTitle>Invite member</DialogTitle>
          <DialogDescription>
            They receive an email invitation for the Aurora workspace.
          </DialogDescription>
        </DialogHeader>
        <Form onSubmit={handleSubmit} className="grid">
          <DialogBody>
            <Field>
              <FieldLabel>Email address</FieldLabel>
              <FieldControl
                name="email"
                type="email"
                placeholder="teammate@example.com"
                required
                disabled={saving}
              />
            </Field>
          </DialogBody>
          <DialogFooter>
            <DialogClose render={<Button variant="ghost" />} disabled={saving}>
              Cancel
            </DialogClose>
            <Button type="submit" disabled={saving}>
              {saving ? (
                <LoaderCircleIcon className="animate-spin" aria-hidden="true" />
              ) : null}
              Send invite
            </Button>
          </DialogFooter>
        </Form>
      </DialogPopup>
    </Dialog>
  );
}

```

### Open from a Menu [#open-from-a-menu]

```tsx
"use client"

import * as React from "react"

import { Button } from "@/components/honest-ui/ui/button"
import {
  Dialog,
  DialogClose,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogPopup,
  DialogTitle,
} from "@/components/honest-ui/ui/dialog"
import {
  Menu,
  MenuItem,
  MenuPopup,
  MenuTrigger,
} from "@/components/honest-ui/ui/menu"

export function DialogFromMenuDemo() {
  const [dialogOpen, setDialogOpen] = React.useState(false)
  return (
    <>
      <Menu>
        <MenuTrigger render={<Button variant="secondary" />}>
          Open menu
        </MenuTrigger>
        <MenuPopup align="start">
          <MenuItem onClick={() => setDialogOpen(true)}>
            Open dialog
          </MenuItem>
        </MenuPopup>
      </Menu>
      <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
        <DialogPopup>
          <DialogHeader>
            <DialogTitle>Settings</DialogTitle>
            <DialogDescription>
              Change your preferences
            </DialogDescription>
          </DialogHeader>
          <DialogFooter>
            <DialogClose render={<Button variant="ghost" />}>
              Close
            </DialogClose>
          </DialogFooter>
        </DialogPopup>
      </Dialog>
    </>
  )
}

```

### Nested Dialogs [#nested-dialogs]

```tsx
import { Button } from "@/components/honest-ui/ui/button"
import {
  Dialog,
  DialogBody,
  DialogClose,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogPopup,
  DialogTitle,
  DialogTrigger,
} from "@/components/honest-ui/ui/dialog"
import { Field, FieldControl, FieldLabel } from "@/components/honest-ui/ui/field"

export function DialogNestedDemo() {
  return (
    <Dialog>
      <DialogTrigger render={<Button variant="secondary" />}>
        Open parent
      </DialogTrigger>
      <DialogPopup showCloseButton={false}>
        <DialogHeader>
          <DialogTitle>Manage team member</DialogTitle>
          <DialogDescription>
            View and manage a user in your team.
          </DialogDescription>
        </DialogHeader>
        <DialogBody className="grid gap-4">
          <div className="grid gap-1">
            <p className="text-sm text-muted-foreground">Name</p>
            <p className="text-sm font-medium">Connor Love</p>
          </div>
          <div className="grid gap-1">
            <p className="text-sm text-muted-foreground">Email</p>
            <p className="text-sm font-medium">loveconnor2005@gmail.com</p>
          </div>
        </DialogBody>
        <DialogFooter>
          <Dialog>
            <DialogTrigger render={<Button variant="secondary" />}>
              Edit details
            </DialogTrigger>
            <DialogPopup showCloseButton={false}>
              <DialogHeader>
                <DialogTitle>Edit details</DialogTitle>
                <DialogDescription>
                  Make changes to the member&apos;s information.
                </DialogDescription>
              </DialogHeader>
              <DialogBody className="flex flex-col gap-4">
                <Field>
                  <FieldLabel>Name</FieldLabel>
                  <FieldControl type="text" defaultValue="Connor Love" />
                </Field>
                <Field>
                  <FieldLabel>Email</FieldLabel>
                  <FieldControl type="text" defaultValue="loveconnor2005@gmail.com" />
                </Field>
              </DialogBody>
              <DialogFooter>
                <DialogClose render={<Button variant="ghost" />}>
                  Cancel
                </DialogClose>
                <Button type="submit">Save changes</Button>
              </DialogFooter>
            </DialogPopup>
          </Dialog>
        </DialogFooter>
      </DialogPopup>
    </Dialog>
  )
}

```

### Close Confirmation [#close-confirmation]

With unsaved input, closing asks whether to discard first.

```tsx
"use client"

import * as React from "react"

import {
  AlertDialog,
  AlertDialogBody,
  AlertDialogClose,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogPopup,
  AlertDialogTitle,
} from "@/components/honest-ui/ui/alert"
import { Button } from "@/components/honest-ui/ui/button"
import {
  Dialog,
  DialogBody,
  DialogClose,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogPopup,
  DialogTitle,
  DialogTrigger,
} from "@/components/honest-ui/ui/dialog"
import { Field } from "@/components/honest-ui/ui/field"
import { Form } from "@/components/honest-ui/ui/form"
import { Textarea } from "@/components/honest-ui/ui/textarea"

export function DialogCloseConfirmationDemo() {
  const [dialogOpen, setDialogOpen] = React.useState(false)
  const [confirmOpen, setConfirmOpen] = React.useState(false)
  const [value, setValue] = React.useState("")

  return (
    <Dialog
      open={dialogOpen}
      onOpenChange={(o) => {
        if (!o && value) {
          setConfirmOpen(true)
        } else {
          setDialogOpen(o)
        }
      }}
    >
      <DialogTrigger render={<Button variant="secondary" />}>
        Compose
      </DialogTrigger>
      <DialogPopup showCloseButton={false}>
        <DialogHeader>
          <DialogTitle>New message</DialogTitle>
          <DialogDescription>
            Type something and try closing.
          </DialogDescription>
        </DialogHeader>
        <Form
          className="grid"
          onSubmit={(event) => {
            event.preventDefault()
            // Close the dialog when submitting
            setDialogOpen(false)
          }}
        >
          <DialogBody>
            <Field>
              <Textarea
                value={value}
                onChange={(e) => setValue(e.target.value)}
              />
            </Field>
          </DialogBody>
          <DialogFooter>
            <DialogClose render={<Button variant="ghost" />}>
              Cancel
            </DialogClose>
            <Button
              onClick={() => {
                setValue("")
                setDialogOpen(false)
              }}
            >
              Send
            </Button>
          </DialogFooter>
        </Form>
      </DialogPopup>

      {/* Confirmation dialog */}
      <AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
        <AlertDialogPopup>
          <AlertDialogHeader>
            <AlertDialogTitle>Discard changes?</AlertDialogTitle>
          </AlertDialogHeader>
          <AlertDialogBody>
            <AlertDialogDescription>
              Your message will be lost.
            </AlertDialogDescription>
          </AlertDialogBody>
          <AlertDialogFooter>
            <AlertDialogClose render={<Button variant="ghost" />}>
              Go back
            </AlertDialogClose>
            <Button
              onClick={() => {
                setConfirmOpen(false)
                setValue("")
                setDialogOpen(false)
              }}
            >
              Discard
            </Button>
          </AlertDialogFooter>
        </AlertDialogPopup>
      </AlertDialog>
    </Dialog>
  )
}

```

## API reference [#api-reference]

All Dialog parts forward their matching Base UI props. `DialogOverlay` aliases `DialogBackdrop`, and `DialogContent` aliases `DialogPopup`. Use the root for controlled (`open` / `onOpenChange`) or uncontrolled open state, the trigger to open it, and `DialogClose` for explicit dismissal. The popup accepts `initialFocus` to direct where focus lands when it opens, and `showCloseButton` to toggle the built-in dismiss affordance.

See the [Base UI Dialog API](https://base-ui.com/react/components/dialog#api-reference).
