Dialog

The panel of a modal, and nothing else. Put it inside a Layer, mount a Header on top of it, and you have a dialog.

API

Props

proptypedefault
as?ElementType"div"the element or component to render
className?stringmerged over the defaults, so it wins
…restprops of astyped against the chosen element

Dialog.Body

The only part that scrolls: it takes the space the header and the footer leave.

proptypedefault
as?ElementType"div"the element or component to render
className?stringmerged over the defaults, so it wins
…restprops of astyped against the chosen element

Dialog.Footer

The actions, aligned to the end and separated by a rule.

proptypedefault
as?ElementType"div"the element or component to render
className?stringmerged over the defaults, so it wins
…restprops of astyped against the chosen element

Examples

Usage

A dialog is three components stacked: Layer is the behaviour, Dialog is the surface, Header is the bar on top. None of them knows about the others, which is why the same Header works on a sheet or on a mobile screen.

tsx
const { close, id, isActive, open } = useGraftDialog<void>();

<Layer open={isActive} onClose={close} aria-labelledby={id}>
  <Dialog className="max-w-lg">
    <Header className="border-b-2 border-border">
      <Header.Title id={id} title="Delete invoice" />
      <Header.Trailing>
        <Button aria-label="close" onClickBlur={close}>
          <Button.Icon icon={<MdClose />} />
        </Button>
      </Header.Trailing>
    </Header>
    <Dialog.Body>This cannot be undone.</Dialog.Body>
    <Dialog.Footer>
      <Button onClickBlur={close}>
        <Button.Label label="cancel" />
      </Button>
    </Dialog.Footer>
  </Dialog>
</Layer>
previewEsc and the backdrop close them

Details

On top of Layer

Dialog is a surface: it does not open, close or trap anything. All of that is Layer, which has its own page and can carry a panel of your own just as well.

Naming the dialog follows from that split: pass an id to Header.Title and the same id as aria-labelledby on the Layer. Nothing is generated behind your back, and a dialog with no visible title can be named with aria-label instead.

The panel is a column

The panel is a column: it is capped at the height of the layer, hides its own overflow, and lets Dialog.Body take the remaining space. That is what keeps a header and a footer fixed while only the middle scrolls — and it is all the layout the component owns.

The footer aligns its actions to the end, which is the common case and not the only one: a destructive action set apart from the confirming one wants the two pushed to opposite sides, and that is a className.

tsx
<Dialog.Footer className="justify-between">
  <Button onClickBlur={remove}>
    <Button.Label label="delete" />
  </Button>
  <Button active onClickBlur={close}>
    <Button.Label label="done" />
  </Button>
</Dialog.Footer>