Dialog
A window over the page for forms and focused tasks, with a pinned header and footer, nesting, and a swipeable bottom sheet on phones.
pnpm dlx shadcn@latest add https://hextaui.com/r/dialog.jsonAdds the component, the HextaUI theme tokens and any HextaUI components it depends on.
Add the theme tokens to your global CSS, if you haven’t yet.
Install the dependencies.
pnpm add @base-ui/react @tabler/icons-react class-variance-authority cnCopy and paste the following code into your project.
components/ui/dialog.tsx components/ui/sheet.tsx components/ui/button.tsx Update the import paths to match your project setup.
Keep the form in <DialogBody /> and point the submit button in the footer at it with form. Enter submits, and the dialog closes through onOpenChange once the value is saved.
Hide the corner button with showCloseButton={false} on the content and add a Close button to the footer with showCloseButton.
Without a button the dialog still closes with Esc, a click outside, or a swipe down on phones.
size sets the maximum width on larger screens: sm, default or lg.
Long content in <DialogBody /> scrolls while the header and footer stay in place, so the actions are always in reach.
Without a footer the body scrolls under the header and keeps its bottom padding.
Pass open and onOpenChange to open it from code, without a trigger.
A dialog or alert dialog opened from inside another stacks on top. The parent steps back and a lighter backdrop covers it. Esc closes only the top one.
Create a handle with createDialogHandle() to share one dialog between many triggers. Each trigger passes a payload that the dialog reads through a render function.
Pass dir to the content as well, because it renders in a portal outside your RTL container.
| Key | Action |
|---|---|
| EnterSpace | On the trigger, opens the dialog and moves focus to its first control. |
| TabShift+Tab | Moves focus between controls. Focus stays inside. |
| Esc | Closes the top dialog and returns focus to its trigger. |
- The content has
role="dialog"and is labelled by its title and described by its description. Always include a<DialogTitle />. - With a mouse or keyboard, focus starts on the first control. With touch it starts on the dialog itself, so the on-screen keyboard doesn’t cover the content before people choose a field. Pass
initialFocusto change it. - The corner close button is labelled “Close”, and the page behind is inert and can’t scroll.
- On phones it becomes a bottom sheet that can be swiped down to close. With reduced motion on, it fades instead of scaling or sliding.
Built on the Base UI drawer, through <Sheet />. Every part accepts the props of the primitive or element it wraps.
| Prop | Type | Default |
|---|---|---|
open | boolean | – |
defaultOpen | boolean | false |
onOpenChange | (open: boolean, details) => void | – |
onOpenChangeCompleteCalled after the open or close animation. | (open: boolean) => void | – |
modal | boolean | "trap-focus" | true |
disablePointerDismissalKeeps the dialog open on clicks outside. | boolean | false |
handleConnects detached triggers. | DialogHandle<Payload> | – |
actionsRefClose or unmount the dialog imperatively. | RefObject<{ close, unmount }> | – |
children | ReactNode | ({ payload }) => ReactNode | – |
| Prop | Type | Default |
|---|---|---|
handle | DialogHandle<Payload> | – |
payloadPassed to the dialog’s render function. | Payload | – |
render | ReactElement | (props, state) => ReactElement | <button> |
| Attribute | Description |
|---|---|
data-slot="dialog-trigger" | Target triggers in CSS. |
data-popup-open | Present while its dialog is open. |
| Prop | Type | Default |
|---|---|---|
size | "sm" | "default" | "lg" | "default" |
showCloseButtonShows the close button in the corner. | boolean | true |
initialFocus | boolean | RefObject | (openType) => HTMLElement | boolean | First control, or the dialog on touch |
finalFocus | boolean | RefObject | (closeType) => HTMLElement | boolean | The trigger |
dirSet it when the dialog should be right to left. | "ltr" | "rtl" | – |
render | ReactElement | (props, state) => ReactElement | <div> |
| Attribute | Description |
|---|---|
data-slot="dialog-content" | Target the dialog in CSS. |
data-size | The current size. |
data-open | Present while open. |
data-starting-style | Present while the dialog animates in. |
data-ending-style | Present while the dialog animates out. |
data-nested-drawer-open | Present while a nested dialog is open on top. |
data-swiping | Present while it is being swiped on a phone. |
--nested-drawers | How many dialogs are open on top of this one. |
A <div> that stacks the title and description. It leaves room for the close button.
| Attribute | Description |
|---|---|
data-slot="dialog-header" | Target the header in CSS. |
A <div> that scrolls when the content is taller than the screen, keeping the header and footer in place.
| Attribute | Description |
|---|---|
data-slot="dialog-body" | Target the body in CSS. |
A <div> for the actions. Buttons stack full width on phones, with the first one at the bottom.
| Prop | Type | Default |
|---|---|---|
showCloseButtonAdds an outline Close button after the children. | boolean | false |
| Attribute | Description |
|---|---|
data-slot="dialog-footer" | Target the footer in CSS. |
| Prop | Type | Default |
|---|---|---|
render | ReactElement | (props, state) => ReactElement | <h2> |
| Attribute | Description |
|---|---|
data-slot="dialog-title" | Target the title in CSS. |
| Prop | Type | Default |
|---|---|---|
renderUse render={<div />} when it holds several paragraphs. | ReactElement | (props, state) => ReactElement | <p> |
| Attribute | Description |
|---|---|
data-slot="dialog-description" | Target the description in CSS. |
| Prop | Type | Default |
|---|---|---|
render | ReactElement | (props, state) => ReactElement | <button> |
| Attribute | Description |
|---|---|
data-slot="dialog-close" | Target close buttons in CSS. |
<DialogContent /> already renders both. Use them only when composing a custom popup.
| Prop | Type | Default |
|---|---|---|
keepMountedOn the portal, keeps the dialog in the DOM while closed. | boolean | false |
Returns a handle that connects <DialogTrigger /> elements anywhere on the page to one <Dialog />. Type the payload with a generic: createDialogHandle<{ name: string }>().
- ButtonButtons in every variant and size, with a built-in loading, success and error flow that skips the spinner for fast requests.
- SheetA panel that slides in from any edge, with swipe-to-dismiss, scroll lock and stacked nesting.
- Alert dialogA confirmation dialog for destructive or important actions that waits for async work, and becomes a bottom sheet on phones.
- CommandA searchable list of actions, inline or as a ⌘K palette, with pages, shortcuts and highlighted matches.
- Context menuA menu of actions on right click or long press, with submenus, checkbox and radio items, and hold feedback on touch.
- DrawerA panel that slides in from any edge and follows your finger, with snap points, a working handle and nested drawers that stack.
Used in Pro blocks
Blocks that build on Dialog.
- API keysThe API keys page of an AI product. Handles a secret shown only once, revoke with undo and rotation with a grace period.
- BillingThe plan and usage page of an AI product. Handles credits running out mid-cycle, plan changes with proration and card errors.
- ModelsThe Models page of an AI product’s settings. Handles the default model and effort, providers and custom servers with a connection test.
- NotificationsThe Notifications section of an AI product’s settings. Handles a channel by event grid, quiet hours and browser permission for push.
- ProfileThe Profile section of an AI product’s settings. Handles photo cropping, a username checked as you type and email changes confirmed by code.
- SecurityThe Security section of an AI product’s settings. Handles sessions, two-factor with recovery codes and deletion behind a typed confirmation.
Every block comes with HextaUI Pro, and Prompt Input is free.