moduix

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.

Base UI API

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.

PropertyDefaultDescription
--dialog-backdrop-bgvar(--backdrop-bg, var(--color-overlay))Controls backdrop color.
--dialog-backdrop-transitionvar(--transition-default)Controls backdrop transition.
--dialog-bgvar(--color-popover)Controls popup background color.
--dialog-border-colorvar(--color-border)Controls popup border color.
--dialog-border-widthvar(--border-width-sm)Controls popup border width.
--dialog-close-icon-bgtransparentControls icon close button background.
--dialog-close-icon-bg-hovervar(--color-accent)Controls icon close hover background.
--dialog-close-icon-colorvar(--dialog-muted-color)Controls icon close color.
--dialog-close-icon-color-hovervar(--dialog-close-icon-color, var(--dialog-color))Controls icon close hover color.
--dialog-close-icon-focus-ring-colorvar(--dialog-focus-ring-color)Controls icon close focus ring color.
--dialog-close-icon-glyph-size0.75remControls icon close glyph size.
--dialog-close-icon-radiusvar(--radius-md)Controls icon close border radius.
--dialog-close-icon-size1.75remControls icon close button size.
--dialog-close-outside-offset-rightvar(--spacing-4)Controls outside close icon right offset.
--dialog-close-outside-offset-topvar(--spacing-4)Controls outside close icon top offset.
--dialog-colorvar(--color-popover-foreground)Controls popup text color.
--dialog-content-marginvar(--spacing-4) 0 0Controls `DialogBody` margin.
--dialog-control-bgvar(--color-background)Controls trigger and close background.
--dialog-control-bg-hovervar(--color-accent)Controls trigger and close hover background.
--dialog-control-border-colorvar(--color-border)Controls trigger and close border color.
--dialog-control-border-widthvar(--border-width-sm)Controls trigger and close border width.
--dialog-control-colorvar(--color-foreground)Controls trigger and close text color.
--dialog-control-font-sizevar(--text-md)Controls trigger and close font size.
--dialog-control-heightvar(--size-lg)Controls trigger and close min height.
--dialog-control-line-heightvar(--line-height-text-md)Controls trigger and close line height.
--dialog-control-padding-x0.875remControls trigger and close horizontal padding.
--dialog-control-padding-y0.5remControls trigger and close vertical padding.
--dialog-control-radiusvar(--radius-md)Controls trigger and close border radius.
--dialog-description-colorvar(--dialog-muted-color)Controls description/body text color.
--dialog-description-font-sizevar(--text-md)Controls description/body font size.
--dialog-description-line-heightvar(--line-height-text-md)Controls description/body line height.
--dialog-focus-ring-colorvar(--color-ring)Controls focus ring color.
--dialog-focus-ring-widthvar(--dialog-control-border-width)Controls focus ring width.
--dialog-footer-gapvar(--spacing-2)Controls footer actions gap.
--dialog-footer-margin-topvar(--spacing-6)Controls footer top margin.
--dialog-header-gapvar(--spacing-1)Controls header row and column gap.
--dialog-max-widthcalc(100vw - var(--spacing-8))Controls popup max width.
--dialog-muted-colorvar(--color-muted-foreground)Controls muted text fallback color.
--dialog-nested-offset-y1.25remControls vertical offset for nested dialogs.
--dialog-nested-overlay-bgrgb(0 0 0 / 0.05)Controls nested parent overlay color.
--dialog-nested-scale-step0.1Controls scale step for nested dialogs.
--dialog-paddingvar(--spacing-6)Controls popup padding.
--dialog-radiusvar(--radius-lg)Controls popup border radius.
--dialog-scalevar(--scale-popup)Controls enter/exit popup scale.
--dialog-shadowvar(--shadow-lg)Controls popup shadow.
--dialog-title-colorvar(--dialog-color)Controls title color.
--dialog-title-font-sizevar(--text-lg)Controls title font size.
--dialog-title-font-weightvar(--weight-semibold)Controls title font weight.
--dialog-title-line-heightvar(--line-height-text-lg)Controls title line height.
--dialog-transitionvar(--transition-default)Controls popup transition.
--dialog-viewport-paddingvar(--spacing-4)Controls viewport padding.
--dialog-width28remControls popup width.

Interactive variables scoped for docs preview without changing size scale tokens.

PropertyValueDefaultDescription
--dialog-backdrop-bgvar(--backdrop-bg, var(--color-overlay))Controls backdrop.
--dialog-bgvar(--color-popover)Controls popup background color.
--dialog-border-colorvar(--color-border)Controls popup border color.
--dialog-colorvar(--color-popover-foreground)Controls popup text color.
--dialog-radiusvar(--radius-lg)Controls the popup border radius.
--dialog-shadowvar(--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>
PartRole
DialogRoot state and interaction model. Use open, defaultOpen, onOpenChange, and modal here.
DialogTriggerOpens the dialog from user interaction. Use handle when the trigger lives outside the tree.
DialogContentPopup container. It renders the internal portal, backdrop, and viewport slots automatically.
DialogHeaderGroups title-level content at the top of the popup.
DialogTitleAccessible dialog title announced by assistive technology.
DialogDescriptionOptional supporting text announced with the title.
DialogBodyOptional content region for forms, text, or custom layout.
DialogFooterOptional actions row for submit/cancel buttons.
DialogCloseIconOptional icon-only close button. Pass children to replace the default icon.
DialogCloseCloses 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);}

On this page