Dialog

A modal surface for focused tasks, confirmations, and stepped flows — built on Radix Dialog.

The Dialog family is a set of four related primitives that all trap focus and present content above the page. Dialog is the low-level Radix wrapper with its own overlay, close button, and escape handling. DialogContainer is the opinionated shell we use for most workspace dialogs, with a titled header, a scrollable body, and a bordered footer already wired up. ConfirmationPopover is a small inline popover for “Are you sure?” moments anchored to the button that triggered the action. NavigableDialog is a larger stepped surface with a left-hand nav for multi-pane flows.

Reach for Dialog when you want full control over structure. Reach for DialogContainer when the task fits the standard header/body/footer layout and you do not want to re-derive the chrome. Use ConfirmationPopover for destructive confirmations that should feel local to the trigger. Use NavigableDialog when the flow has distinct sections (settings, wizards, multi-step forms) that users need to move between without losing the surrounding context.

Import

import {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogFooter,
  DialogClose,
  DialogContainer,
  ConfirmPopover,
  NavigableDialogRoot,
  NavigableDialogHeader,
  NavigableDialogBody,
  NavigableDialogNav,
  NavigableDialogContent,
  NavigableDialogFooter,
} from "@unkey/ui";

Dialog

Dialog is the Radix primitive with default chrome. Compose it from DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, and DialogClose. The content renders through a portal, so it escapes overflow: hidden ancestors, and it ships with a built-in close button in the top-right corner.

Controlled open state is the common pattern. Drive it with useState so the dialog can be opened from outside the trigger (for example, from a menu item elsewhere in the page) and closed imperatively once an action completes.

DialogContainer

DialogContainer is a styled wrapper around DialogContent. It accepts title, subTitle, footer, and children and renders the standard workspace chrome: bordered header, content area with scroll, bordered footer. The width steps through responsive breakpoints up to a 600px cap.

Use it when the dialog is essentially a form or a short flow with a single set of actions. For anything more elaborate (multi-step, side navigation), step up to NavigableDialog instead.

showCloseWarning combined with onAttemptClose lets you intercept the close intent. Escape key, overlay click, and the X button all route through onAttemptClose instead of closing the dialog directly, so you can anchor a ConfirmPopover to the X button before losing unsaved input. preventOutsideClose is the blunter tool. It suppresses overlay clicks entirely, which matters when a secondary surface (a sheet, a command menu) is layered on top.

ConfirmationPopover

ConfirmPopover is a popover, not a dialog, but it lives in the dialog family because it solves the destructive-action confirmation case. It anchors to a triggerRef you pass in, renders a small titled panel with a confirm and cancel button, and closes automatically after the user confirms.

Use it for revocations, deletions, and any other action where a full modal would be overkill. Pair the danger variant with a danger-colored button on the trigger.

NavigableDialog is the composed, stepped version. The root provides a shared active-step context, NavigableDialogNav renders the left-hand rail, and NavigableDialogContent cross-fades between panes keyed by the same ids. Navigation can be gated with onNavigate, which returns a boolean (or a promise of one) so you can validate the current pane before letting the user leave it.

Panes are all mounted at once and toggled with opacity and translate, which keeps form state alive between steps without remounting inputs. If your panes are expensive to render, guard them with a condition on activeId.

Accessibility

Every variant in this family delegates to Radix Dialog or Radix Popover, so the heavy lifting is already done. The content root carries role="dialog" with aria-modal="true", focus is trapped inside on open, and focus returns to the trigger on close. Escape closes the dialog unless showCloseWarning intercepts it, and the built-in X button has an explicit aria-label.

Label every dialog by putting a DialogTitle inside DialogContent. Radix wires aria-labelledby automatically when the title is present. Pair it with DialogDescription for supporting copy so aria-describedby points somewhere meaningful. For DialogContainer the title prop is always rendered, so the labelling happens by default. For NavigableDialog, mount a NavigableDialogHeader at the top for the same reason.

ConfirmPopover is not a modal. It does not trap focus. Screen reader users reach the confirm button via the normal tab order after the trigger, so keep the title and description short and unambiguous.

Props

Dialog

Root state container. A thin re-export of Radix.Dialog.Root.

open boolean Optional

Controlled open state. Pair with onOpenChange.

defaultOpen boolean Optional

Initial open state for uncontrolled use.

onOpenChange (open: boolean) => void Optional

Fires when the open state changes, whether by trigger, escape, overlay click, or programmatic control.

modal boolean Optional Default true

When true, the dialog traps focus and blocks pointer interaction with everything underneath. Set to false for a non-blocking surface (rare).

DialogTrigger

The element that opens the dialog. Almost always rendered with asChild so an existing Button keeps its semantics.

asChild boolean Optional Default false

Merge the trigger behavior into a single child instead of rendering a wrapping <button>.

children ReactNode Optional

The trigger element. With asChild, must be a single element that accepts a ref and pointer handlers.

DialogContent

The portal-rendered modal surface. Carries role="dialog", aria-modal="true", the animated overlay, and the close button.

showCloseWarning boolean Optional Default false

When true, escape, overlay click, and the X button call onAttemptClose instead of closing. Use to guard unsaved input.

onAttemptClose () => void Optional

Fires when the user tries to close while showCloseWarning is set. Open a ConfirmPopover or swap the X for a confirmation flow here.

xButtonRef RefObject<HTMLButtonElement> Optional

Ref to the built-in X button. Used as the anchor when pairing with ConfirmPopover on attempted close.

preventOutsideClose boolean Optional Default false

Suppress overlay clicks and interact-outside events. Keep enabled when a secondary surface (sheet, command menu) is layered above.

className string Optional

Additional Tailwind classes. Merged with the default chrome via cn.

children ReactNode Optional

Typically DialogHeader, body content, and DialogFooter.

DialogHeader

Flex column with tight spacing. Sits at the top of DialogContent.

className string Optional

Additional Tailwind classes.

children ReactNode Optional

Usually DialogTitle and/or DialogDescription.

DialogTitle

Renders as the Radix title. Wire up aria-labelledby automatically.

className string Optional

Additional Tailwind classes.

children ReactNode Optional

A short phrase. Keep it under one line.

DialogDescription

Renders as the Radix description. Wires up aria-describedby automatically.

className string Optional

Additional Tailwind classes.

children ReactNode Optional

One or two sentences of supporting copy.

DialogFooter

Horizontal flex row on desktop, reversed column on mobile. For action buttons.

className string Optional

Additional Tailwind classes. Common overrides include justify-between.

children ReactNode Optional

Usually one or more Buttons, with the primary action last.

DialogClose

Closes the surrounding dialog when clicked. Render with asChild to wrap an existing Button.

asChild boolean Optional Default false

Merge the close behavior into a single child instead of rendering a wrapping <button>.

children ReactNode Optional

The element that should close the dialog on click.

DialogContainer

Pre-styled wrapper that owns its own Dialog and DialogContent and renders the standard header, body, and footer layout.

isOpen boolean Required

Controlled open state.

onOpenChange (value: boolean) => void Required

Fires when the open state changes.

title string Required

Header title. Renders inside the accessible DialogTitle.

subTitle string Optional

Optional secondary line beneath the title.

footer ReactNode Optional

Content for the bordered footer. Usually one or more action buttons. If omitted, the footer area is not rendered at all.

children ReactNode Optional

The body content. Rendered in a scrollable area between the header and the footer.

className string Optional

Additional Tailwind classes for the outer DialogContent.

contentClassName string Optional

Additional Tailwind classes for the inner scroll container.

preventAutoFocus boolean Optional Default false

Suppress Radix’s initial autofocus. Use when the first focusable element would be a destructive button or an expensive side effect.

modal boolean Optional Default true

Forwarded to the underlying Dialog. Set to false for a non-blocking surface.

showCloseWarning boolean Optional Default false

Route close attempts through onAttemptClose instead of closing.

onAttemptClose () => void Optional

Fires when the user tries to close while showCloseWarning is set.

preventOutsideClose boolean Optional Default false

Suppress overlay clicks and interact-outside events.

ConfirmPopover

Popover-based confirmation for destructive or warning actions. Anchored to a ref you provide, not to its own trigger.

isOpen boolean Required

Controlled open state.

onOpenChange (open: boolean) => void Required

Fires when the open state changes.

onConfirm () => void Required

Fires when the user presses the confirm button. The popover closes automatically afterwards.

triggerRef RefObject<HTMLElement | null> Required

Element the popover anchors to. Usually the button that opened it, or the X button of a surrounding dialog.

title string Optional Default "Confirm action"

Heading shown at the top of the popover.

description string Optional Default "Are you sure you want to perform this action?"

Supporting copy between the divider and the buttons.

confirmButtonText string Optional Default "Confirm"

Label for the primary action button.

cancelButtonText string Optional Default "Cancel"

Label for the ghost cancel button.

variant "warning" | "danger" Optional Default "warning"

Visual tone. warning pairs an amber icon with an amber button. danger pairs a red icon with a red button.

popoverProps Partial<PopoverContentProps> Optional

Forwarded to the underlying PopoverContent. Use to override side, align, sideOffset, or className.

Outer shell for the stepped dialog. Provides the active-step context and the Radix dialog primitives.

isOpen boolean Required

Controlled open state.

onOpenChange (value: boolean) => void Required

Fires when the open state changes.

children ReactNode Required

The dialog layout. Expect NavigableDialogHeader, NavigableDialogBody (wrapping Nav + Content), and NavigableDialogFooter.

dialogClassName string Optional

Additional Tailwind classes for the DialogContent. Use to override the default width.

preventAutoFocus boolean Optional Default false

Suppress initial autofocus on open.

Styled title block. Thin wrapper around the shared default header.

title string Required

Main heading.

subTitle string Optional

Optional secondary line.

Flex container that places NavigableDialogNav beside NavigableDialogContent.

children ReactNode Required

Typically a NavigableDialogNav followed by a NavigableDialogContent.

className string Optional

Additional Tailwind classes.

Left-hand rail of buttons. Each item has an id that must match a content item’s id.

items { id: string; label: ReactNode; icon?: FC<IconProps> }[] Required

Navigation entries. The active item gets a filled background, and a leading icon is optional per item.

onNavigate (fromId: string) => boolean | Promise<boolean> Optional

Gate leaving the current pane. Return false (or a promise that resolves to false) to cancel the navigation, for example when a form in the current pane is invalid.

initialSelectedId string Optional

Starting pane. Falls back to the first item when omitted or unknown.

disabledIds string[] Optional

Items rendered but not interactive. Useful for steps that unlock only after prior steps complete.

navWidthClass string Optional Default "w-[220px]"

Tailwind width class for the nav column.

className string Optional

Additional Tailwind classes for the nav column.

Cross-fading pane container. Items are keyed by the same id as the nav.

items { id: string; content: ReactNode }[] Required

The panes themselves. All panes are mounted at once and toggled with opacity and translate, which preserves form state between steps.

className string Optional

Additional Tailwind classes for the scroll area.

Bordered footer. Wraps the shared default footer styling.

children ReactNode Required

Usually the primary and secondary action buttons for the whole flow.

  • Drawer — when the surface should slide in from an edge instead of centering over the page; better on mobile and for side-by-side context.
  • Popover — for small, non-modal floating content attached to a trigger, without focus trapping.
  • Alert — for inline, non-interrupting notices that belong in the document flow rather than on a modal surface.