Skip to content
UI

Dialog

A window over the page that asks the user to complete a short task without leaving the current view.

@zenoxai/ui/components/dialogBase UI Markdown
<Dialog>
  <DialogTrigger render={<Button variant="outline" />}>
    Rename asset
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Rename asset</DialogTitle>
      <DialogDescription>
        The new name appears in alerts, reports and the asset inventory.
      </DialogDescription>
    </DialogHeader>
    <FieldGroup>
      <Field>
        <FieldLabel htmlFor="asset-name">Name</FieldLabel>
        <Input id="asset-name" defaultValue="srv-db-01.prod" />
      </Field>
      <Field>
        <FieldLabel htmlFor="asset-owner">Owner</FieldLabel>
        <Input id="asset-owner" defaultValue="Infrastructure team" />
      </Field>
    </FieldGroup>
    <DialogFooter>
      <DialogClose render={<Button variant="outline" />}>
        Cancel
      </DialogClose>
      <Button>Save changes</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>

Usage

import { Button } from "@zenoxai/ui/components/button"
import {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@zenoxai/ui/components/dialog"
<Dialog>
  <DialogTrigger render={<Button variant="outline" />}>
    Rename asset
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Rename asset</DialogTitle>
      <DialogDescription>
        The new name appears in alerts and reports.
      </DialogDescription>
    </DialogHeader>
    {/* fields */}
    <DialogFooter>
      <DialogClose render={<Button variant="outline" />}>Cancel</DialogClose>
      <Button>Save changes</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>

Use a dialog for a short, focused task that blocks the page until it is done or dismissed: renaming a record, creating a tenant, reviewing a list before export. The page behind it is inert while it is open. Render the trigger as a Button through render, so it keeps the button’s look and the trigger’s behavior.

Pick another overlay when the task does not fit that shape:

Need Use
Confirm an irreversible or consequential action Alert Dialog
A long form, or details next to the list that opened them Sheet
A panel on touch screens that the user swipes away Drawer
A few controls anchored to the button that opened them, page stays usable Popover

DialogContent renders the portal, the backdrop and a close button in the top-right corner. Hide the close button with showCloseButton={false} only when the footer offers another way out. DialogFooter sits flush with the bottom edge on a muted band; pass showCloseButton to it to add a “Close” button after its children.

Examples

Sizes

size sets the maximum width from the sm breakpoint up: sm (25rem, the default), md (35rem), lg (50rem) and xl (68.75rem). Use sm for one or two fields, md for forms and lists, lg and xl for tables or side-by-side content.

<>
  {(["sm", "md", "lg", "xl"] as const).map((size) => (
    <Dialog key={size}>
      <DialogTrigger render={<Button variant="outline" />}>
        Open {size}
      </DialogTrigger>
      <DialogContent size={size}>
        <DialogHeader>
          <DialogTitle>Dialog size {size}</DialogTitle>
          <DialogDescription>
            The size sets the maximum width from the sm breakpoint up. Below
            it, every size fills the screen minus a 1rem margin.
          </DialogDescription>
        </DialogHeader>
        <DialogFooter showCloseButton />
      </DialogContent>
    </Dialog>
  ))}
</>

Scrollable content

The dialog grows with its content up to the height of the viewport less a 1rem margin on each side, then scrolls as a whole, footer included. To keep the title and the actions in view, give the long part a maximum height and overflow-y-auto, and keep the header and footer outside it.

<Dialog>
  <DialogTrigger render={<Button variant="outline" />}>
    View findings
  </DialogTrigger>
  <DialogContent size="md">
    <DialogHeader>
      <DialogTitle>Weekly vulnerability report</DialogTitle>
      <DialogDescription>
        24 findings across 3 tenants, sorted by severity.
      </DialogDescription>
    </DialogHeader>
    <ul className="-mx-4 max-h-80 overflow-y-auto border-y border-border px-4 py-2">
      {Array.from({ length: 24 }, (_, index) => (
        <li
          key={index}
          className="flex justify-between gap-4 border-b border-border py-2 last:border-0"
        >
          <span>CVE-2024-{3094 + index}</span>
          <span className="text-text-secondary">
            srv-web-{String(index + 1).padStart(2, "0")}.prod
          </span>
        </li>
      ))}
    </ul>
    <DialogFooter showCloseButton>
      <Button>Export PDF</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>

Controlled

Control open when the dialog must stay open while a request runs and close only when it succeeds. Disable the fields and the buttons while it is pending.

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

  function save() {
    setSaving(true)
    // Stands in for the request that saves the tenant.
    setTimeout(() => {
      setSaving(false)
      setOpen(false)
    }, 1200)
  }

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger render={<Button />}>New tenant</DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>New tenant</DialogTitle>
          <DialogDescription>
            Tenants isolate assets, alerts and users of one customer.
          </DialogDescription>
        </DialogHeader>
        <Field>
          <FieldLabel htmlFor="tenant-name">Name</FieldLabel>
          <Input id="tenant-name" placeholder="Acme Corp" disabled={saving} />
        </Field>
        <DialogFooter>
          <DialogClose render={<Button variant="outline" />} disabled={saving}>
            Cancel
          </DialogClose>
          <Button onClick={save} disabled={saving}>
            {saving && <Spinner data-icon="inline-start" />}
            {saving ? "Creating" : "Create tenant"}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  )
}

Accessibility

  • Focus moves into the dialog when it opens, stays inside while it is open and returns to the trigger when it closes. Escape and a click on the backdrop close it.
  • Always render a DialogTitle: it is the dialog’s accessible name. DialogDescription is read after it. If the design has no visible title, keep it and hide it with className="sr-only".
  • The built-in close button is an icon button labelled “Close” for screen readers.

API reference

Generated from the TypeScript source. className, style and children are accepted everywhere and omitted below.

Dialog

Dialog is a modal window over the page for a short, focused task: a small form, a choice, a preview. It traps focus and closes on Escape or an outside click. To confirm a destructive action, use AlertDialog or `confirm()` from the ConfirmDialog pattern; for a long form or a record's details beside a list, Sheet; on touch screens, Drawer.

PropTypeDefault
actionsRefRefObject<DialogRootActions | null>

A ref to imperative actions. - `unmount`: Ends the closing phase of the dialog after an externally controlled closing animation finishes. Call `preventUnmountOnClose()` in `onOpenChange` first, otherwise the dialog completes closing on its own. Whether it leaves the DOM is decided by `keepMounted` on the portal. - `close`: Closes the dialog imperatively when called.

-
defaultOpenboolean

Whether the dialog is initially open.

false
defaultTriggerIdstring | null

ID of the trigger that the dialog is associated with. This is useful in conjunction with the `defaultOpen` prop to create an initially open dialog.

-
disablePointerDismissalboolean

Whether to prevent the dialog from closing on outside presses. For non-modal dialogs, this also prevents the dialog from closing when focus moves outside of it.

false
handleDialogHandle<unknown>

A handle to associate the dialog with a trigger. If specified, allows external triggers to control the dialog's open state. Can be created with the Dialog.createHandle() method.

-
modalboolean | "trap-focus"

Determines if the dialog enters a modal state when open. - `true`: user interaction is limited to just the dialog: focus is trapped, document page scroll is locked, and pointer interactions on outside elements are disabled. - `false`: user interaction with the rest of the document is allowed. - `'trap-focus'`: focus is trapped inside the dialog, but document page scroll is not locked and pointer interactions outside of it remain enabled.

true
onOpenChange((open: boolean, eventDetails: DialogRootChangeEventDetails) => void)

Event handler called when the dialog is opened or closed.

-
onOpenChangeComplete((open: boolean) => void)

Event handler called after any animations complete when the dialog is opened or closed.

-
openboolean

Whether the dialog is currently open.

-
triggerIdstring | null

ID of the trigger that the dialog is associated with. This is useful in conjunction with the `open` prop to create a controlled dialog. There's no need to specify this prop when the dialog is uncontrolled (that is, when the `open` prop is not set).

-

DialogClose

DialogClose closes the dialog when pressed; pass `render={<Button />}` to style it as a button.

PropTypeDefault
nativeButtonboolean

Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`).

true
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DialogCloseState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-

DialogContent

DialogContent is the dialog box, centered in a portal over a dimmed backdrop, with a close button at its top-right corner. It is never taller than the viewport less a 16px margin on each side: longer content scrolls inside it, footer included.

PropTypeDefault
finalFocusboolean | RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)

Determines the element to focus when the dialog is closed.

-
initialFocusboolean | RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null)

Determines the element to focus when the dialog is opened. By default, focus moves to the first tabbable element inside the popup, except when the dialog is opened by touch — then the popup itself is focused to avoid opening the virtual keyboard.

-
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DialogPopupState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-
showCloseButtonboolean

Renders the X button at the top-right corner. Its accessible name is the English "Close": to translate it, turn this off and render your own DialogClose.

true
size"sm" | "lg" | "md" | "xl"

Maximum width from the sm breakpoint: `sm` 400px (default), `md` 560px, `lg` 800px, `xl` 1100px. Below it the dialog spans the viewport less a 16px margin on each side.

sm

DialogDescription

PropTypeDefault
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DialogDescriptionState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-

DialogFooter

DialogFooter is the action bar on a muted band at the bottom of the dialog: buttons aligned right from the sm breakpoint, and stacked below it with the last one on top.

PropTypeDefault
showCloseButtonboolean

Appends an outline button that closes the dialog. Its text is the English "Close"; in other languages render a translated DialogClose instead.

false

DialogHeader

No props of its own. Accepts the props of the element it renders.

DialogOverlay

DialogOverlay is the dimmed, blurred backdrop. DialogContent renders it already.

PropTypeDefault
forceRenderboolean

Whether the backdrop is forced to render even when nested.

false
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DialogBackdropState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-

DialogPortal

DialogPortal moves the dialog to the end of the document body. DialogContent renders it already.

PropTypeDefault
containerHTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null

A parent element to render the portal element into.

-
keepMountedboolean

Whether to keep the portal mounted in the DOM while the popup is hidden.

false
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DialogPortalState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-

DialogTitle

PropTypeDefault
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DialogTitleState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-

DialogTrigger

PropTypeDefault
handleDialogHandle<unknown>

A handle to associate the trigger with a dialog. Can be created with the Dialog.createHandle() method.

-
idstring

ID of the trigger. In addition to being forwarded to the rendered element, it is also used to specify the active trigger for the dialog in controlled mode (with the DialogRoot `triggerId` prop).

-
nativeButtonboolean

Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`).

true
payloadunknown

A payload to pass to the dialog when it is opened.

-
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DialogTriggerState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-