Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

OverlaysReact

Dialog

A popup that opens on top of the entire page.

Rendered by React 19.3.1

Loading React…
import { Dialog } from '@hamur/react/dialog'

export default function DialogExample() {
  return (
    <Dialog.Root>
      <Dialog.Trigger className="button">
        Open dialog
      </Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Backdrop className="backdrop" />
        <Dialog.Popup className="dialog">
          <Dialog.Title className="dialog-title">
            Discard draft?
          </Dialog.Title>
          <Dialog.Description className="muted">
            Your changes will be lost. This cannot be undone.
          </Dialog.Description>
          <div className="actions">
            <Dialog.Close className="button">
              Keep editing
            </Dialog.Close>
            <Dialog.Close className="button danger">
              Discard
            </Dialog.Close>
          </div>
        </Dialog.Popup>
      </Dialog.Portal>
    </Dialog.Root>
  )
}
782Tests passing in React · 0 failing
28.5 KiBSize
1.10 msOpen
1.40 msClose
18.0 msOpenMutations
20.0 msCloseMutations
0.85 MiBMemory
productdevbook and 22 upstream authors28 commits · last

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<Dialog.Root>
  <Dialog.Trigger />
  <Dialog.Portal>
    <Dialog.Backdrop />
    <Dialog.Popup>
      <Dialog.Title />
      <Dialog.Description />
      <Dialog.Close />
    </Dialog.Popup>
  </Dialog.Portal>
</Dialog.Root>

// Not in this example: Dialog.Viewport

Examples

Nested

A dialog opens another; the parent styles itself with data-nested-dialog-open and --nested-dialogs.

DialogNested.tsx
import { Dialog } from '@hamur/react/dialog'

export default function DialogNestedExample() {
  return (
    <Dialog.Root>
      <Dialog.Trigger className="button">View notifications</Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Backdrop className="backdrop" />
        <Dialog.Popup className="dialog dialog-stacked">
          <div className="dialog-intro">
            <Dialog.Title className="dialog-title">Notifications</Dialog.Title>
            <Dialog.Description className="muted">
              You are all caught up. Good job!
            </Dialog.Description>
          </div>
          <div className="actions start">
            <Dialog.Root>
              <Dialog.Trigger className="button">Customize</Dialog.Trigger>
              <Dialog.Portal>
                <Dialog.Popup className="dialog dialog-stacked">
                  <div className="dialog-intro">
                    <Dialog.Title className="dialog-title">Customize notifications</Dialog.Title>
                    <Dialog.Description className="muted">
                      Review your settings here.
                    </Dialog.Description>
                  </div>
                  <div className="actions">
                    <Dialog.Close className="button">Close</Dialog.Close>
                  </div>
                </Dialog.Popup>
              </Dialog.Portal>
            </Dialog.Root>
          </div>
        </Dialog.Popup>
      </Dialog.Portal>
    </Dialog.Root>
  )
}

Focus Management

initialFocus and finalFocus choose where focus goes on open and on close.

DialogFocusManagement.tsx
import * as React from 'react'
import { Dialog } from '@hamur/react/dialog'

export default function DialogFocusManagementExample() {
  const initialFocusRef = React.useRef<HTMLInputElement | null>(null)
  const finalFocusRef = React.useRef<HTMLButtonElement | null>(null)
  const nameId = React.useId()
  const feedbackId = React.useId()

  return (
    <div className="button-row">
      <Dialog.Root>
        <Dialog.Trigger className="button">Open feedback</Dialog.Trigger>
        <Dialog.Portal>
          <Dialog.Backdrop className="backdrop" />
          <Dialog.Popup
            className="dialog"
            initialFocus={initialFocusRef}
            finalFocus={finalFocusRef}
          >
            <div className="dialog-intro">
              <Dialog.Title className="dialog-title">Feedback form</Dialog.Title>
              <Dialog.Description className="muted">
                Your feedback means a lot to us.
              </Dialog.Description>
            </div>
            <div className="dialog-fields">
              <div className="field">
                <label htmlFor={nameId}>Full name</label>
                <input id={nameId} placeholder="Enter your name" className="input" />
              </div>
              <div className="field">
                <label htmlFor={feedbackId}>Feedback</label>
                <input
                  id={feedbackId}
                  ref={initialFocusRef}
                  required
                  placeholder="Enter your feedback"
                  className="input"
                />
              </div>
            </div>
            <div className="actions">
              <Dialog.Close className="button">Close</Dialog.Close>
            </div>
          </Dialog.Popup>
        </Dialog.Portal>
      </Dialog.Root>
      <button ref={finalFocusRef} type="button" className="button">
        Final focus
      </button>
    </div>
  )
}

Outside Scroll

Viewport scrolls and the popup grows past the bottom edge.

DialogOutsideScroll.tsx
import * as React from 'react'
import { Dialog } from '@hamur/react/dialog'

export default function DialogOutsideScrollExample() {
  const popupRef = React.useRef<HTMLDivElement>(null)
  return (
    <Dialog.Root>
      <Dialog.Trigger className="button">Open dialog</Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Backdrop className="backdrop" />
        <Dialog.Viewport className="outside-scroll-viewport">
          <div className="outside-scroll-content">
            <Dialog.Popup ref={popupRef} className="outside-scroll-popup" initialFocus={popupRef}>
              <div className="outside-scroll-header">
                <Dialog.Title className="dialog-title">Dialog</Dialog.Title>
                <Dialog.Description className="muted">
                  This layout keeps an outer container scrollable while the dialog can extend past
                  the bottom edge.
                </Dialog.Description>
                <Dialog.Close className="icon-button outside-scroll-close" aria-label="Close">
                  <XIcon />
                </Dialog.Close>
              </div>

              <div className="outside-scroll-body">
                {CONTENT_SECTIONS.map((item) => (
                  <section className="scroll-section" key={item.title}>
                    <h3 className="scroll-section-title">{item.title}</h3>
                    <p className="scroll-section-body">{item.body}</p>
                  </section>
                ))}
              </div>

              <p className="muted">
                Related docs:{' '}
                {RELATED_LINKS.map((item, index) => (
                  <React.Fragment key={item.href}>
                    {index > 0 ? ', ' : null}
                    <a className="footer-link" href={item.href}>
                      {item.label}
                    </a>
                  </React.Fragment>
                ))}
                .
              </p>
            </Dialog.Popup>
          </div>
        </Dialog.Viewport>
      </Dialog.Portal>
    </Dialog.Root>
  )
}

function XIcon() {
  return (
    <svg
      width="16"
      height="16"
      viewBox="0 0 16 16"
      fill="none"
      stroke="currentColor"
      strokeLinecap="square"
      strokeLinejoin="round"
      className="icon"
    >
      <path d="m2.5 2.5 11 11m-11 0 11-11" />
    </svg>
  )
}

const CONTENT_SECTIONS = [
  {
    title: 'What a dialog is for',
    body: 'Use a dialog when you need the user to complete a focused task or read something important without navigating away. It opens on top of the page and returns focus back where it started when closed.',
  },
  {
    title: 'Anatomy at a glance',
    body: 'Root, Trigger, Portal, Backdrop, Viewport, Popup, Title, Description, Close. Keep the title short and the first paragraph specific so screen readers announce something meaningful.',
  },
  {
    title: 'Opening and closing',
    body: 'Control it using external state via the `open` and `onOpenChange` props, or let it manage state for you internally.',
  },
  {
    title: 'Keyboard and focus behavior',
    body: 'Focus moves inside the dialog when it opens. Tab and Shift+Tab loop within, and Esc requests close.',
  },
  {
    title: 'Accessible labeling',
    body: 'Set an explicit title and description using the `Dialog.Title` and `Dialog.Description` components.',
  },
  {
    title: 'Backdrop and page scrolling',
    body: 'The backdrop visually separates layers while background content is inert. Don’t rely on dimness alone—keep copy clear and buttons obvious so actions are easy to choose.',
  },
  {
    title: 'Portals and stacking',
    body: 'Dialogs render in a portal so they sit above the `isolation: isolate` app content and avoid local z-index wars.',
  },
  {
    title: 'Viewport overflow',
    body: 'Let long content overflow the bottom edge and reveal as you scroll the page container. Keep generous padding at the top and bottom so the dialog doesn’t feel jammed against the edges.',
  },
  {
    title: 'Nested dialogs and confirmations',
    body: 'If closing a dialog needs confirmation, open a child alert dialog rather than mutating the current one. The parent stays visible behind it; only the topmost layer should feel interactive.',
  },
  {
    title: 'Transitions that respect motion settings',
    body: 'Use small, fast transitions (opacity plus a few pixels of Y translation or scale). Subtle motion helps people notice what changed without slowing them down.',
  },
  {
    title: 'Controlled vs. uncontrolled',
    body: 'Controlled state is best when other parts of the page need to react to open/close. Uncontrolled is fine for local cases where only the dialog matters.',
  },
  {
    title: 'Close affordances',
    body: 'Always offer a visible close button in the corner. Don’t rely only on Esc or the backdrop for pointer outside presses. Touch screen readers and accessibility users benefit from a clear, targetable control to click to close the dialog.',
  },
  {
    title: 'Forms inside dialogs',
    body: 'Keep forms short; longer flows usually deserve a full page. Validate inline, keep button text specific (“Create project”), and disable destructive actions until the input is valid.',
  },
  {
    title: 'Content guidelines',
    body: 'Lead with the outcome (“Rename project?”) and follow with one or two short, concrete sentences. Avoid long prose; link out for details instead.',
  },
  {
    title: 'SSR and hydration notes',
    body: 'Because dialogs render in a portal, make sure your portal container exists on the client.',
  },
  {
    title: 'Mobile ergonomics',
    body: 'Use larger touch targets and keep the close button reachable with the thumb. Avoid full-screen modals unless the task truly needs a whole screen.',
  },
  {
    title: 'Theming and density',
    body: 'Match spacing and corner radius to your system. Use a slightly denser layout than pages so the dialog feels purpose-built, not like a mini web page.',
  },
  {
    title: 'Internationalization',
    body: 'Plan for longer text. Buttons can grow to two lines; titles should wrap gracefully. Keep destructive terms consistent across locales.',
  },
  {
    title: 'Performance',
    body: 'Children are mounted lazily when the dialog opens. If the dialog can reopen often, consider the `keepMounted` prop sparingly to perform the work only once on mount to avoid re-initializing complex React trees on each open.',
  },
  {
    title: 'When a popover is better',
    body: 'If the content is a small hint or a few quick actions anchored to a control, use a popover or menu instead of a dialog. Dialogs interrupt on purpose—use that sparingly.',
  },
  {
    title: 'Follow-up and cleanup',
    body: 'After a successful action, close the dialog and show confirmation in context (toast, inline message, or updated UI) so people can see the result of what they just did.',
  },
]

const RELATED_LINKS = [
  { href: '/docs?component=scroll-area', label: 'Scroll Area' },
  { href: '/docs?component=drawer', label: 'Drawer' },
  { href: '/docs?component=popover', label: 'Popover' },
] as const

Inside Scroll

Viewport positions the popup; a container inside it scrolls.

DialogInsideScroll.tsx
import { Dialog } from '@hamur/react/dialog'

export default function DialogInsideScrollExample() {
  return (
    <Dialog.Root>
      <Dialog.Trigger className="button">Open dialog</Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Backdrop className="backdrop" />
        <Dialog.Viewport className="inside-scroll-viewport">
          <Dialog.Popup className="inside-scroll-popup">
            <div className="inside-scroll-header">
              <Dialog.Title className="dialog-title">Dialog</Dialog.Title>
              <Dialog.Description className="muted">
                This layout keeps the popup fully on screen while allowing its content to scroll.
              </Dialog.Description>
            </div>
            <div className="inside-scroll-body">
              {CONTENT_SECTIONS.map((item) => (
                <section className="scroll-section" key={item.title}>
                  <h3 className="scroll-section-title">{item.title}</h3>
                  <p className="scroll-section-body">{item.body}</p>
                </section>
              ))}
            </div>
            <div className="inside-scroll-actions">
              <Dialog.Close className="button">Close</Dialog.Close>
            </div>
          </Dialog.Popup>
        </Dialog.Viewport>
      </Dialog.Portal>
    </Dialog.Root>
  )
}

const CONTENT_SECTIONS = [
  {
    title: 'What a dialog is for',
    body: 'Use a dialog when you need the user to complete a focused task or read something important without navigating away. It opens on top of the page and returns focus back where it started when closed.',
  },
  {
    title: 'Anatomy at a glance',
    body: 'Root, Trigger, Portal, Backdrop, Popup, Title/Description, Close. Keep the title short and the first paragraph specific so screen readers announce something meaningful.',
  },
  {
    title: 'Opening and closing',
    body: 'Control it using external state via the `open` and `onOpenChange` props, or let it manage state for you internally.',
  },
  {
    title: 'Keyboard and focus behavior',
    body: 'Focus moves inside the dialog when it opens. Tab and Shift+Tab loop within, and Esc requests close.',
  },
  {
    title: 'Accessible labeling',
    body: 'Set an explicit title and description using the `Dialog.Title` and `Dialog.Description` components.',
  },
  {
    title: 'Backdrop and page scrolling',
    body: 'The backdrop visually separates layers while background content is inert. Don’t rely on dimness alone—keep copy clear and buttons obvious so actions are easy to choose.',
  },
  {
    title: 'Portals and stacking',
    body: 'Dialogs render in a portal so they sit above the `isolation: isolate` app content and avoid local z-index wars.',
  },
  {
    title: 'Viewport overflow',
    body: 'Let long content overflow the bottom edge and reveal as you scroll the page container. Keep generous padding at the top and bottom so the dialog doesn’t feel jammed against the edges.',
  },
  {
    title: 'Nested dialogs and confirmations',
    body: 'If closing a dialog needs confirmation, open a child alert dialog rather than mutating the current one. The parent stays visible behind it; only the topmost layer should feel interactive.',
  },
  {
    title: 'Transitions that respect motion settings',
    body: 'Use small, fast transitions (opacity plus a few pixels of Y translation or scale). Subtle motion helps people notice what changed without slowing them down.',
  },
  {
    title: 'Controlled vs. uncontrolled',
    body: 'Controlled state is best when other parts of the page need to react to open/close. Uncontrolled is fine for local cases where only the dialog matters.',
  },
  {
    title: 'Close affordances',
    body: 'Always offer a visible close button in the corner. Don’t rely only on Esc or the backdrop for pointer outside presses. Touch screen readers and accessibility users benefit from a clear, targetable control to click to close the dialog.',
  },
  {
    title: 'Forms inside dialogs',
    body: 'Keep forms short; longer flows usually deserve a full page. Validate inline, keep button text specific (“Create project”), and disable destructive actions until the input is valid.',
  },
  {
    title: 'Content guidelines',
    body: 'Lead with the outcome (“Rename project?”) and follow with one or two short, concrete sentences. Avoid long prose; link out for details instead.',
  },
  {
    title: 'SSR and hydration notes',
    body: 'Because dialogs render in a portal, make sure your portal container exists on the client.',
  },
  {
    title: 'Mobile ergonomics',
    body: 'Use larger touch targets and keep the close button reachable with the thumb. Avoid full-screen modals unless the task truly needs a whole screen.',
  },
  {
    title: 'Theming and density',
    body: 'Match spacing and corner radius to your system. Use a slightly denser layout than pages so the dialog feels purpose-built, not like a mini web page.',
  },
  {
    title: 'Internationalization',
    body: 'Plan for longer text. Buttons can grow to two lines; titles should wrap gracefully. Keep destructive terms consistent across locales.',
  },
  {
    title: 'Performance',
    body: 'Children are mounted lazily when the dialog opens. If the dialog can reopen often, consider the `keepMounted` prop sparingly to perform the work only once on mount to avoid re-initializing complex React trees on each open.',
  },
  {
    title: 'When a popover is better',
    body: 'If the content is a small hint or a few quick actions anchored to a control, use a popover or menu instead of a dialog. Dialogs interrupt on purpose—use that sparingly.',
  },
  {
    title: 'Follow-up and cleanup',
    body: 'After a successful action, close the dialog and show confirmation in context (toast, inline message, or updated UI) so people can see the result of what they just did.',
  },
]

Uncontained

Content that looks outside the popup stays inside Popup, so it keeps its tab order.

DialogUncontained.tsx
import { Dialog } from '@hamur/react/dialog'

export default function DialogUncontainedExample() {
  return (
    <Dialog.Root>
      <Dialog.Trigger className="button">Open dialog</Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Backdrop className="backdrop" />
        <Dialog.Viewport className="uncontained-viewport">
          <Dialog.Popup className="uncontained-root">
            <Dialog.Close className="uncontained-close" aria-label="Close">
              <XIcon />
            </Dialog.Close>
            <div className="uncontained-popup" />
          </Dialog.Popup>
        </Dialog.Viewport>
      </Dialog.Portal>
    </Dialog.Root>
  )
}

function XIcon() {
  return (
    <svg
      width="16"
      height="16"
      viewBox="0 0 16 16"
      fill="none"
      stroke="currentColor"
      strokeLinecap="square"
      strokeLinejoin="round"
      className="icon"
    >
      <path d="m2.5 2.5 11 11m-11 0 11-11" />
    </svg>
  )
}

Detached Triggers Simple

A trigger outside Root opens it through a handle.

DialogDetachedTriggersSimple.tsx
import * as React from 'react'
import { Dialog } from '@hamur/react/dialog'

const demoDialog = Dialog.createHandle()

export default function DialogDetachedTriggersSimpleExample() {
  return (
    <React.Fragment>
      <Dialog.Trigger className="button" handle={demoDialog}>
        View notifications
      </Dialog.Trigger>

      <Dialog.Root handle={demoDialog}>
        <Dialog.Portal>
          <Dialog.Backdrop className="backdrop" />
          <Dialog.Popup className="dialog">
            <div className="dialog-intro">
              <Dialog.Title className="dialog-title">Notifications</Dialog.Title>
              <Dialog.Description className="muted">
                You are all caught up. Good job!
              </Dialog.Description>
            </div>
            <div className="actions">
              <Dialog.Close className="button">Close</Dialog.Close>
            </div>
          </Dialog.Popup>
        </Dialog.Portal>
      </Dialog.Root>
    </React.Fragment>
  )
}

Detached Triggers Controlled

Several triggers share one controlled dialog and pass a payload.

DialogDetachedTriggersControlled.tsx
import * as React from 'react'
import { Dialog } from '@hamur/react/dialog'

const demoDialog = Dialog.createHandle<number>()

export default function DialogDetachedTriggersControlledExample() {
  const [open, setOpen] = React.useState(false)
  const [triggerId, setTriggerId] = React.useState<string | null>(null)

  const handleOpenChange = (isOpen: boolean, eventDetails: Dialog.Root.ChangeEventDetails) => {
    setOpen(isOpen)
    setTriggerId(eventDetails.trigger?.id ?? null)
  }

  return (
    <React.Fragment>
      <div className="button-row">
        <Dialog.Trigger className="button" handle={demoDialog} id="trigger-1" payload={1}>
          Open 1
        </Dialog.Trigger>
        <Dialog.Trigger className="button" handle={demoDialog} id="trigger-2" payload={2}>
          Open 2
        </Dialog.Trigger>
        <Dialog.Trigger className="button" handle={demoDialog} id="trigger-3" payload={3}>
          Open 3
        </Dialog.Trigger>
        <button
          className="button"
          type="button"
          onClick={() => {
            setTriggerId('trigger-2')
            setOpen(true)
          }}
        >
          Open programmatically
        </button>
      </div>

      <Dialog.Root
        handle={demoDialog}
        open={open}
        onOpenChange={handleOpenChange}
        triggerId={triggerId}
      >
        {({ payload }) => (
          <Dialog.Portal>
            <Dialog.Backdrop className="backdrop" />
            <Dialog.Popup className="dialog">
              {payload !== undefined && (
                <Dialog.Title className="dialog-title">Dialog {payload}</Dialog.Title>
              )}
              <div className="actions">
                <Dialog.Close className="button">Close</Dialog.Close>
              </div>
            </Dialog.Popup>
          </Dialog.Portal>
        )}
      </Dialog.Root>
    </React.Fragment>
  )
}

Open From Menu

A menu item opens the dialog.

DialogOpenFromMenu.tsx
import * as React from 'react'
import { Dialog } from '@hamur/react/dialog'
import { Menu } from '@hamur/react/menu'

export default function DialogOpenFromMenuExample() {
  const [dialogOpen, setDialogOpen] = React.useState(false)

  return (
    <React.Fragment>
      <Menu.Root>
        <Menu.Trigger className="button">
          Playlist <CaretDownIcon />
        </Menu.Trigger>
        <Menu.Portal>
          <Menu.Positioner sideOffset={8} align="start">
            <Menu.Popup className="menu">
              <Menu.Item className="menu-item">Play</Menu.Item>
              <Menu.Item className="menu-item">Share</Menu.Item>
              <Menu.Separator className="menu-separator" />
              {/* Open the dialog when the menu item is clicked */}
              <Menu.Item className="menu-item" onClick={() => setDialogOpen(true)}>
                Details…
              </Menu.Item>
            </Menu.Popup>
          </Menu.Positioner>
        </Menu.Portal>
      </Menu.Root>

      {/* Control the dialog state */}
      <Dialog.Root open={dialogOpen} onOpenChange={setDialogOpen}>
        <Dialog.Portal>
          <Dialog.Backdrop className="backdrop" />
          <Dialog.Popup className="dialog">
            <div className="dialog-intro">
              <Dialog.Title className="dialog-title">Playlist details</Dialog.Title>
              <Dialog.Description className="muted">
                This playlist contains 24 songs and was last updated today.
              </Dialog.Description>
            </div>
            <div className="actions">
              <Dialog.Close className="button">Close</Dialog.Close>
            </div>
          </Dialog.Popup>
        </Dialog.Portal>
      </Dialog.Root>
    </React.Fragment>
  )
}

function CaretDownIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" className="icon">
      <path d="M12 6H4l4 4.5z" />
    </svg>
  )
}

API reference

21 props across 9 parts, in React syntax. className, style, render are listed once, under Shared props.

BackdropDialog.Backdrop

An overlay displayed beneath the popup. Renders a <div>.

Backdrop props
PropTypeDefault
forceRenderWhether the backdrop is forced to render even when nested.booleanfalse
Backdrop data attributes
Data attributeDescription
data-openPresent when the dialog is open.
data-closedPresent when the dialog is closed.
data-starting-stylePresent when the dialog begins animating in.
data-ending-stylePresent when the dialog is animating out.

CloseDialog.Close

A button that closes the dialog. Renders a <button>.

Close props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
Close data attributes
Data attributeDescription
data-disabledPresent when the button is disabled.

DescriptionDialog.Description

A paragraph with additional information about the dialog. Renders a <p>.

PopupDialog.Popup

A container for the dialog contents. Renders a <div>.

Popup props
PropTypeDefault
initialFocusDetermines the element to focus when the dialog is opened.boolean | React.RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null)—
finalFocusDetermines the element to focus when the dialog is closed.boolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)—
Popup data attributes
Data attributeDescription
data-openPresent when the dialog is open.
data-closedPresent when the dialog is closed.
data-starting-stylePresent when the dialog begins animating in.
data-ending-stylePresent when the dialog is animating out.
data-nestedPresent when the dialog is nested within another dialog.
data-nested-dialog-openPresent when the dialog has other open dialogs nested within it.
Popup CSS variables
CSS variableDescription
--nested-dialogsIndicates how many dialogs are nested within.

PortalDialog.Portal

A portal element that moves the popup to a different part of the DOM. By default, the portal element is appended to <body>. Renders a <div>.

Portal props
PropTypeDefault
containerA parent element to render the portal element into.HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null—
keepMountedWhether to keep the portal mounted in the DOM while the popup is hidden.booleanfalse

RootDialog.Root

Groups all parts of the dialog. Doesn’t render its own element.

Root props
PropTypeDefault
defaultOpenWhether the dialog is initially open.booleanfalse
openWhether the dialog is currently open.boolean—
onOpenChangeEvent handler called when the dialog is opened or closed.((open: boolean, eventDetails: DialogRootChangeEventDetails) => void)—
actionsRefA ref to imperative actions.React.RefObject<DialogRootActions | null>—
defaultTriggerIdID of the trigger that the dialog is associated with.string | null—
disablePointerDismissalWhether to prevent the dialog from closing on outside presses.booleanfalse
handleA handle to associate the dialog with a trigger.DialogHandle<Payload>—
modalDetermines if the dialog enters a modal state when open.boolean | "trap-focus"true
onOpenChangeCompleteEvent handler called after any animations complete when the dialog is opened or closed.((open: boolean) => void)—
triggerIdID of the trigger that the dialog is associated with.string | null—
childrenThe content of the dialog.React.ReactNode | PayloadChildRenderFunction<Payload>—

TitleDialog.Title

A heading that labels the dialog. Renders a <h2>.

TriggerDialog.Trigger

A button that opens the dialog. Renders a <button>.

Trigger props
PropTypeDefault
handleA handle to associate the trigger with a dialog.DialogHandle<Payload>—
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
payloadA payload to pass to the dialog when it is opened.Payload—
idID of the trigger.string—
Trigger data attributes
Data attributeDescription
data-disabledPresent when the trigger is disabled.
data-popup-openPresent when the corresponding dialog is open.

ViewportDialog.Viewport

A positioning container for the dialog popup that can be made scrollable. Renders a <div>.

Viewport data attributes
Data attributeDescription
data-openPresent when the dialog is open.
data-closedPresent when the dialog is closed.
data-starting-stylePresent when the dialog begins animating in.
data-ending-stylePresent when the dialog is animating out.
data-nestedPresent when the dialog is nested within another dialog.
data-nested-dialog-openPresent when the dialog has other open dialogs nested within it.

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
EscapeCloses the dialog and returns focus to the triggerPopup
EscapeWith several dialogs open, closes only the one on topPopup
TabShiftTabMoves between the popup's controls; focus wraps inside while modal is onPopup

Accessibility

What the parts set
PartSetsSo that
Triggeraria-haspopup="dialog", aria-expanded, aria-controlsa screen reader says the button opens a dialog and whether it is open
Popuprole="dialog"the popup is announced as a dialog
Popuparia-labelledby to Title, aria-describedby to Descriptionthe dialog is announced with its heading and its description
Popuparia-hidden="true" on the rest of the page while modal is ona screen reader stays inside the dialog
Backdroprole="presentation"the overlay is not read
Viewportrole="presentation"the positioning container is not read

Focus order

  1. Opening moves focus to the first focusable element in Popup (to Popup itself when opened by touch), or to initialFocus.
  2. Tab and Shift+Tab move through the popup's controls and wrap at either end while the dialog is modal.
  3. Closing returns focus to the Trigger that opened it, or to finalFocus.