Dialog
A modal dialog that opens above the page for focused tasks and contextual workflows.
API Reference
Original primitive API
Behavior, accessibility details, and low-level props are documented by Base UI.
Basic
import { Dialog, DialogTrigger, Button, DialogContent, DialogHeader, DialogTitle, DialogCloseIcon, DialogDescription, DialogFooter, DialogClose,} from "moduix";export function DialogDemo() { return ( <Dialog> <DialogTrigger render={<Button />}>View notifications</DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Notifications</DialogTitle> <DialogCloseIcon /> <DialogDescription>You are all caught up. Good job!</DialogDescription> </DialogHeader> <DialogFooter> <DialogClose render={<Button variant="outline" />}>Close</DialogClose> </DialogFooter> </DialogContent> </Dialog> );}Full list of component variables available for project-level overrides.
| Property | Default | Description |
|---|---|---|
| --dialog-backdrop-bg | var(--backdrop-bg, var(--color-overlay)) | Controls backdrop color. |
| --dialog-backdrop-transition | var(--transition-default) | Controls backdrop transition. |
| --dialog-bg | var(--color-popover) | Controls popup background color. |
| --dialog-border-color | var(--color-border) | Controls popup border color. |
| --dialog-border-width | var(--border-width-sm) | Controls popup border width. |
| --dialog-close-icon-bg | transparent | Controls icon close button background. |
| --dialog-close-icon-bg-hover | var(--color-accent) | Controls icon close hover background. |
| --dialog-close-icon-color | var(--dialog-muted-color) | Controls icon close color. |
| --dialog-close-icon-color-hover | var(--dialog-close-icon-color, var(--dialog-color)) | Controls icon close hover color. |
| --dialog-close-icon-focus-ring-color | var(--dialog-focus-ring-color) | Controls icon close focus ring color. |
| --dialog-close-icon-glyph-size | 0.75rem | Controls icon close glyph size. |
| --dialog-close-icon-radius | var(--radius-md) | Controls icon close border radius. |
| --dialog-close-icon-size | 1.75rem | Controls icon close button size. |
| --dialog-close-outside-offset-right | var(--spacing-4) | Controls outside close icon right offset. |
| --dialog-close-outside-offset-top | var(--spacing-4) | Controls outside close icon top offset. |
| --dialog-color | var(--color-popover-foreground) | Controls popup text color. |
| --dialog-content-margin | var(--spacing-4) 0 0 | Controls `DialogBody` margin. |
| --dialog-control-bg | var(--color-background) | Controls trigger and close background. |
| --dialog-control-bg-hover | var(--color-accent) | Controls trigger and close hover background. |
| --dialog-control-border-color | var(--color-border) | Controls trigger and close border color. |
| --dialog-control-border-width | var(--border-width-sm) | Controls trigger and close border width. |
| --dialog-control-color | var(--color-foreground) | Controls trigger and close text color. |
| --dialog-control-font-size | var(--text-md) | Controls trigger and close font size. |
| --dialog-control-height | var(--size-lg) | Controls trigger and close min height. |
| --dialog-control-line-height | var(--line-height-text-md) | Controls trigger and close line height. |
| --dialog-control-padding-x | 0.875rem | Controls trigger and close horizontal padding. |
| --dialog-control-padding-y | 0.5rem | Controls trigger and close vertical padding. |
| --dialog-control-radius | var(--radius-md) | Controls trigger and close border radius. |
| --dialog-description-color | var(--dialog-muted-color) | Controls description/body text color. |
| --dialog-description-font-size | var(--text-md) | Controls description/body font size. |
| --dialog-description-line-height | var(--line-height-text-md) | Controls description/body line height. |
| --dialog-focus-ring-color | var(--color-ring) | Controls focus ring color. |
| --dialog-focus-ring-width | var(--dialog-control-border-width) | Controls focus ring width. |
| --dialog-footer-gap | var(--spacing-2) | Controls footer actions gap. |
| --dialog-footer-margin-top | var(--spacing-6) | Controls footer top margin. |
| --dialog-header-gap | var(--spacing-1) | Controls header row and column gap. |
| --dialog-max-width | calc(100vw - var(--spacing-8)) | Controls popup max width. |
| --dialog-muted-color | var(--color-muted-foreground) | Controls muted text fallback color. |
| --dialog-nested-offset-y | 1.25rem | Controls vertical offset for nested dialogs. |
| --dialog-nested-overlay-bg | rgb(0 0 0 / 0.05) | Controls nested parent overlay color. |
| --dialog-nested-scale-step | 0.1 | Controls scale step for nested dialogs. |
| --dialog-padding | var(--spacing-6) | Controls popup padding. |
| --dialog-radius | var(--radius-lg) | Controls popup border radius. |
| --dialog-scale | var(--scale-popup) | Controls enter/exit popup scale. |
| --dialog-shadow | var(--shadow-lg) | Controls popup shadow. |
| --dialog-title-color | var(--dialog-color) | Controls title color. |
| --dialog-title-font-size | var(--text-lg) | Controls title font size. |
| --dialog-title-font-weight | var(--weight-semibold) | Controls title font weight. |
| --dialog-title-line-height | var(--line-height-text-lg) | Controls title line height. |
| --dialog-transition | var(--transition-default) | Controls popup transition. |
| --dialog-viewport-padding | var(--spacing-4) | Controls viewport padding. |
| --dialog-width | 28rem | Controls popup width. |
Interactive variables scoped for docs preview without changing size scale tokens.
| Property | Value | Default | Description |
|---|---|---|---|
| --dialog-backdrop-bg | var(--backdrop-bg, var(--color-overlay)) | Controls backdrop. | |
| --dialog-bg | var(--color-popover) | Controls popup background color. | |
| --dialog-border-color | var(--color-border) | Controls popup border color. | |
| --dialog-color | var(--color-popover-foreground) | Controls popup text color. | |
| --dialog-radius | var(--radius-lg) | Controls the popup border radius. | |
| --dialog-shadow | var(--shadow-lg) | Controls popup shadow. |
Anatomy
Dialog combines a root state machine with one visible popup surface and internal service layers.
Use DialogContent as the popup container; it automatically renders portal, backdrop, and
viewport slots for overlay behavior.
Dialog
├─ DialogTrigger
└─ DialogContent
├─ portal (service slot)
│ ├─ backdrop (service slot)
│ └─ viewport (service slot)
│ └─ popup surface
│ ├─ DialogCloseIcon (optional)
│ ├─ DialogHeader
│ │ ├─ DialogTitle
│ │ └─ DialogDescription (optional)
│ ├─ DialogBody (optional)
│ └─ DialogFooter (optional)
│ └─ DialogClose<Dialog>
<DialogTrigger render={<Button />}>Edit profile</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogCloseIcon />
<DialogDescription>Update your public information.</DialogDescription>
</DialogHeader>
<DialogBody>
<p>Profile fields and form controls go here.</p>
</DialogBody>
<DialogFooter>
<DialogClose render={<Button variant="outline" />}>Cancel</DialogClose>
<DialogClose render={<Button />}>Save</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>| Part | Role |
|---|---|
Dialog | Root state and interaction model. Use open, defaultOpen, onOpenChange, and modal here. |
DialogTrigger | Opens the dialog from user interaction. Use handle when the trigger lives outside the tree. |
DialogContent | Popup container. It renders the internal portal, backdrop, and viewport slots automatically. |
DialogHeader | Groups title-level content at the top of the popup. |
DialogTitle | Accessible dialog title announced by assistive technology. |
DialogDescription | Optional supporting text announced with the title. |
DialogBody | Optional content region for forms, text, or custom layout. |
DialogFooter | Optional actions row for submit/cancel buttons. |
DialogCloseIcon | Optional icon-only close button. Pass children to replace the default icon. |
DialogClose | Closes the dialog from action buttons while preserving built-in behavior. |
In most cases, keep default overlay behavior and style only visible popup parts with className
(DialogContent, DialogHeader, DialogBody, DialogFooter). Customize service slots with
classNames (portal, backdrop, viewport) only when you need special layering, positioning,
or overlay visuals.
Composition
Use Dialog for root state and behavior props such as open, defaultOpen, onOpenChange, and
modal. Base UI root behavior is passed through: onOpenChangeComplete,
disablePointerDismissal, actionsRef, handle, triggerId, and defaultTriggerId are available
on Dialog.
className styles the visible popup. classNames styles the internal service slots that are
hidden from the default composition. Use container, slotProps, and withBackdrop={false} when
you need the matching Base UI escape hatches. Common cases are slotProps.portal.keepMounted,
slotProps.backdrop.forceRender, and viewport attributes or refs through slotProps.viewport.
DialogContent itself accepts popup props such as initialFocus and finalFocus.
<DialogContent
className={styles.popup}
classNames={{
portal: styles.portal,
backdrop: styles.backdrop,
viewport: styles.viewport,
}}
slotProps={{
portal: { keepMounted: true },
backdrop: { forceRender: true },
}}
/>DialogTrigger, DialogClose, DialogTitle, DialogDescription, and DialogContent also accept
their matching Base UI primitive props, including render where Base UI supports element
replacement.
Examples
Controlled
Control the open state from React when the dialog needs to coordinate with other UI.
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose,} from "moduix";import { useState, Fragment } from "react";export function ControlledDialogDemo() { const [open, setOpen] = useState(false); return ( <Fragment> <Button type="button" onClick={() => setOpen(true)}> Open controlled dialog </Button> <Dialog open={open} onOpenChange={setOpen}> <DialogContent> <DialogHeader> <DialogTitle>Publish changes?</DialogTitle> <DialogDescription> This will make the latest version visible to all users. </DialogDescription> </DialogHeader> <DialogFooter> <DialogClose render={<Button variant="outline" />}>Back to editing</DialogClose> <DialogClose render={<Button />}>Publish</DialogClose> </DialogFooter> </DialogContent> </Dialog> </Fragment> );}Handle
Use createDialogHandle when the trigger lives outside the dialog tree or the dialog opens
programmatically.
import { createDialogHandle, DialogTrigger, Button, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose,} from "moduix";import { useMemo, Fragment } from "react";export function DialogHandleDemo() { const dialogHandle = useMemo(() => createDialogHandle(), []); return ( <Fragment> <DialogTrigger handle={dialogHandle} render={<Button variant="outline" />}> Open from detached trigger </DialogTrigger> <Button type="button" onClick={() => dialogHandle.open(null)}> Open programmatically </Button> <Dialog handle={dialogHandle}> <DialogContent> <DialogHeader> <DialogTitle>Detached trigger</DialogTitle> <DialogDescription> This dialog is connected via createDialogHandle(). </DialogDescription> </DialogHeader> <DialogFooter> <DialogClose render={<Button variant="outline" />}>Close</DialogClose> </DialogFooter> </DialogContent> </Dialog> </Fragment> );}Scrollable Content
Place ScrollArea inside DialogBody when the header and footer should remain visible while only
the body content scrolls.
import { Dialog, DialogTrigger, Button, DialogContent, DialogHeader, DialogTitle, DialogCloseIcon, DialogDescription, DialogBody, DialogFooter, DialogClose, ScrollArea,} from "moduix";export function ScrollableDialogDemo() { return ( <Dialog> <DialogTrigger render={<Button />}>Open long content</DialogTrigger> <DialogContent className={styles.scrollPopup}> <DialogHeader> <DialogTitle>Release checklist</DialogTitle> <DialogCloseIcon /> <DialogDescription> Review all items before publishing to production. </DialogDescription> </DialogHeader> <DialogBody className={styles.scrollBody}> <ScrollArea className={styles.scrollArea} classNames={{ content: styles.scrollContent }} > {sections.map((item) => ( <section key={item.title}> <h3>{item.title}</h3> <p>{item.body}</p> </section> ))} </ScrollArea> </DialogBody> <DialogFooter> <DialogClose render={<Button variant="outline" />}>Close</DialogClose> </DialogFooter> </DialogContent> </Dialog> );}.scrollPopup { display: flex; flex-direction: column; height: min(42rem, calc(100dvh - var(--spacing-10))); overflow: hidden;}.scrollBody { min-height: 0; flex: 1; overflow: hidden;}.scrollArea { height: 100%; min-height: 0;}.scrollContent { display: flex; flex-direction: column; gap: var(--spacing-5);}const sections = [ { title: "Database migrations", body: "Confirm migration scripts are idempotent and have rollback steps.", }, { title: "Monitoring", body: "Check that the dashboard includes new API endpoints.", }, { title: "Analytics", body: "Verify onboarding funnel events are firing.", }, { title: "Staging", body: "Run smoke tests with a production-like dataset.", },];Nested
Dialogs can be nested. The parent popup receives Base UI nested-dialog attributes and CSS variables, so it can visually recede while the child dialog is active.
import { Dialog, DialogTrigger, Button, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose,} from "moduix";export function NestedDialogDemo() { return ( <Dialog> <DialogTrigger render={<Button />}>View notifications</DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Notifications</DialogTitle> <DialogDescription>You are all caught up. Good job!</DialogDescription> </DialogHeader> <DialogFooter> <Dialog> <DialogTrigger render={<Button variant="outline" />}>Customize</DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Customize notifications</DialogTitle> <DialogDescription>Review your settings here.</DialogDescription> </DialogHeader> <DialogFooter> <DialogClose render={<Button variant="outline" />}>Close</DialogClose> </DialogFooter> </DialogContent> </Dialog> <DialogClose render={<Button variant="outline" />}>Close</DialogClose> </DialogFooter> </DialogContent> </Dialog> );}Non-Modal
Set modal={false} when the dialog should not trap focus, lock page scroll, or block pointer
interaction with the rest of the page. Pair it with withBackdrop={false} when no overlay should be
rendered.
import { Dialog, DialogTrigger, Button, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose,} from "moduix";export function NonModalDialogDemo() { return ( <Dialog modal={false}> <DialogTrigger render={<Button />}>Open non-modal dialog</DialogTrigger> <DialogContent withBackdrop={false}> <DialogHeader> <DialogTitle>Non-modal dialog</DialogTitle> <DialogDescription> The page remains interactive because modal behavior and backdrop are disabled. </DialogDescription> </DialogHeader> <DialogFooter> <DialogClose render={<Button variant="outline" />}>Close</DialogClose> </DialogFooter> </DialogContent> </Dialog> );}Custom Styles
For an outside close pattern, place DialogCloseIcon directly in DialogContent and position it
against the viewport corner. This keeps the close control independent from popup size and loading
content behavior.
import { Dialog, DialogTrigger, Button, DialogContent, DialogCloseIcon, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter, DialogClose, CloseLineIcon,} from "moduix";export function CustomStylesDialogDemo() { return ( <Dialog> <DialogTrigger render={<Button />}>Open outside close icon</DialogTrigger> <DialogContent className={styles.customPopup} classNames={{ portal: styles.customPortal, backdrop: styles.customBackdrop, viewport: styles.customViewport, }} slotProps={{ portal: { keepMounted: true }, backdrop: { forceRender: true }, }} > <DialogCloseIcon aria-label="Close profile dialog" className={styles.customCloseIcon} > <CloseLineIcon /> </DialogCloseIcon> <DialogHeader> <DialogTitle>Edit profile</DialogTitle> <DialogDescription> This popup, backdrop, viewport, and close icon are styled with className and classNames. </DialogDescription> </DialogHeader> <DialogBody> <p>Update the public profile fields and save changes.</p> </DialogBody> <DialogFooter> <DialogClose render={<Button />}>Save</DialogClose> </DialogFooter> </DialogContent> </Dialog> );}.customPortal { pointer-events: auto;}.customBackdrop { background-color: rgb(15 23 42 / 0.56);}.customViewport { align-items: start; padding-top: var(--spacing-10);}.customPopup { position: relative; overflow: visible; --dialog-width: 26rem; --dialog-padding: var(--spacing-5);}.customCloseIcon { position: fixed; top: var(--spacing-4); right: var(--spacing-4); border: var(--border-width-sm) solid var(--dialog-border-color, var(--color-border)); background-color: var(--dialog-bg, var(--color-popover)); --dialog-close-icon-color: var(--color-muted-foreground);}