1. Components
  2. Overlay
  3. Dialog

Dialog

A dialog is an overlay shown above other content in an application.

<DialogRoot>
  <Button variant="outline">Edit Profile</Button>
  <Dialog title="Edit profile" description="Make changes to your profile.">
    {({ close }) => (
      <>
        <div>
          <TextField autoFocus label="Name" defaultValue="Mehdi" />
          <TextField label="Username" defaultValue="@mehdibha_" />
        </div>
        <DialogFooter>
          <Button variant="outline" size={{ initial: "lg", sm: "md" }} onPress={close}>
            Cancel
          </Button>
          <Button variant="primary" size={{ initial: "lg", sm: "md" }} onPress={close}>
            Save changes
          </Button>
        </DialogFooter>
      </>
    )}
  </Dialog>
</DialogRoot>

Installation

npx dotui-cli@latest add dialog

Usage

Use Dialog to display contextual information, tasks, or workflows that appear over the user interface. Depending on the kind of dialog, further interactions may be blocked until the dialog is acknowledged.

Options

Title

Dialog is labeled with a title to provide context to the user. Use the title prop to provide a title for the dialog.
If a dialog does not have a visible heading element, an aria-label or aria-labelledby prop must be passed instead to identify the element to assistive technology.

<DialogRoot>
  <Button variant="outline">Edit username</Button>
  <Dialog title="Edit username">
    <TextField label="Username" defaultValue="@mehdibha_" className="w-full" />
  </Dialog>
</DialogRoot>

Description

A description can be supplied to the dialog via the description prop.

<DialogRoot>
  <Button variant="outline">Edit username</Button>
  <Dialog title="Edit username" description="Make changes to your username.">
    <TextField label="Username" defaultValue="@mehdibha_" className="w-full" />
  </Dialog>
</DialogRoot>

Type

Dialogs can be rendered as modals, popovers, or drawers using the type prop. By default, dialogs are displayed as drawers on mobile. You can override this behavior by setting the mobileType prop.

Type
Mobile type
<DialogRoot>
  <Button variant="outline">Edit username</Button>
  <Dialog title="Edit username">
    {({ close }) => (
      <>
        <div className="space-y-4">
          <TextField autoFocus label="Name" defaultValue="Mehdi" className="w-full" />
          <TextField label="Username" defaultValue="@mehdibha_" className="w-full" />
        </div>
        <DialogFooter>
          <Button variant="outline" size={{ initial: "lg", sm: "md" }} onPress={close}>
            Cancel
          </Button>
          <Button variant="primary" size={{ initial: "lg", sm: "md" }} onPress={close}>
            Save changes
          </Button>
        </DialogFooter>
      </>
    )}
  </Dialog>
</DialogRoot>

Dismissable

By default, dialogs are dismissable allowing users to click outside to close the dialog. You can disable this behavior by setting the isDismissable prop.

<DialogRoot>
  <Button variant="outline">Edit Profile</Button>
  <Dialog
    title="Edit profile"
    description="Make changes to your profile."
    isDismissable={false}
  >
    {({ close }) => (
      <>
        <div className="space-y-4">
          <TextField autoFocus label="Name" defaultValue="Mehdi" className="w-full" />
          <TextField label="Username" defaultValue="@mehdibha_" className="w-full" />
        </div>
        <DialogFooter>
          <Button variant="outline" size={{ initial: "lg", sm: "md" }} onPress={close}>
            Cancel
          </Button>
          <Button variant="primary" size={{ initial: "lg", sm: "md" }} onPress={close}>
            Save changes
          </Button>
        </DialogFooter>
      </>
    )}
  </Dialog>
</DialogRoot>

Dismiss button

By default, dialogs have a dismiss button when they're dismissable. You can disable this behavior by setting showDismissButton prop.

<DialogRoot>
  <Button variant="outline">Edit Profile</Button>
  <Dialog
    title="Edit profile"
    description="Make changes to your profile."
    showDismissButton={false}
  >
    {({ close }) => (
      <>
        <div className="space-y-4">
          <TextField autoFocus label="Name" defaultValue="Mehdi" className="w-full" />
          <TextField label="Username" defaultValue="@mehdibha_" className="w-full" />
        </div>
        <DialogFooter>
          <Button variant="outline" size={{ initial: "lg", sm: "md" }} onPress={close}>
            Cancel
          </Button>
          <Button variant="primary" size={{ initial: "lg", sm: "md" }} onPress={close}>
            Save changes
          </Button>
        </DialogFooter>
      </>
    )}
  </Dialog>
</DialogRoot>

Inset content

Use the <DialogInset> component to add inset content to the dialog.

<DialogRoot>
  <Button variant="outline">Edit Profile</Button>
  <Dialog title="Edit profile" description="Make changes to your profile.">
    {({ close }) => (
      <>
        <DialogInset className="mb-4 bg-bg-muted border-y">Content within the inset.</DialogInset>
        <div className="space-y-4">
          <TextField autoFocus label="Name" defaultValue="Mehdi" className="w-full" />
          <TextField label="Username" defaultValue="@mehdibha_" className="w-full" />
        </div>
        <DialogFooter>
          <Button variant="outline" size={{ initial: "lg", sm: "md" }} onPress={close}>
            Cancel
          </Button>
          <Button variant="primary" size={{ initial: "lg", sm: "md" }} onPress={close}>
            Save changes
          </Button>
        </DialogFooter>
      </>
    )}
  </Dialog>
</DialogRoot>

Popover

Use the type="popover" prop on the <Dialog> element to make a popover dialog.

Placement
"use client";

import React from "react";
import type { Key } from "react-aria-components";
import { Button } from "@/registry/ui/default/core/button";
import { DialogRoot, Dialog } from "@/registry/ui/default/core/dialog";
import { Item } from "@/registry/ui/default/core/list-box";
import { NumberField } from "@/registry/ui/default/core/number-field";
import { Select } from "@/registry/ui/default/core/select";
import { Switch } from "@/registry/ui/default/core/switch";
import { InfoIcon } from "@/__icons__";

export default function Demo() {
  const [placement, setPlacement] = React.useState<Key>("top");
  const [offset, setOffset] = React.useState<number>(0);
  const [crossOffset, setCrossOffset] = React.useState<number>(0);
  const [containerPadding, setContainerPadding] = React.useState<number>(0);
  const [showArrow, setShowArrow] = React.useState<boolean>(true);
  return (
    <div className="flex w-full items-center">
      <div className="flex flex-1 items-center justify-center">
        <DialogRoot>
          <Button variant="outline" shape="square">
            <InfoIcon />
          </Button>
          <Dialog
            type="popover"
            title="Help"
            description="For help accessing your account, please contact support."
          />
        </DialogRoot>
      </div>
      <div className="space-y-4 rounded-md border p-4">
        <Select
          label="Placement"
          selectedKey={placement}
          onSelectionChange={setPlacement}
        >
          <Item id="top">Top</Item>
          <Item id="bottom">Bottom</Item>
        </Select>
        <NumberField label="Offset" value={offset} onChange={setOffset} />
        <NumberField
          label="Cross offset"
          value={crossOffset}
          onChange={setCrossOffset}
        />
        <NumberField
          label="Container padding"
          value={containerPadding}
          onChange={setContainerPadding}
        />
        <Switch isSelected={showArrow} onChange={setShowArrow}>
          Arrow
        </Switch>
      </div>
    </div>
  );
}

Drawer

Use the type="drawer" prop on the <Dialog> element to make a drawer dialog.

Placement
"use client";

import React from "react";
import type { Key } from "react-aria-components";
import { Button } from "@/registry/ui/default/core/button";
import { DialogRoot, Dialog } from "@/registry/ui/default/core/dialog";
import { Item } from "@/registry/ui/default/core/list-box";
import { Select } from "@/registry/ui/default/core/select";
import { Switch } from "@/registry/ui/default/core/switch";

export default function Demo() {
  const [placement, setPlacement] = React.useState<Key>("top");
  const [swipeable, setSwipeable] = React.useState<boolean>(true);
  return (
    <div className="flex w-full items-center">
      <div className="flex flex-1 items-center justify-center">
        <DialogRoot>
          <Button variant="outline">Open drawer</Button>
          <Dialog
            type="drawer"
            title="Help"
            description="For help accessing your account, please contact support."
          />
        </DialogRoot>
      </div>
      <div className="space-y-4 rounded-md border p-4">
        <Select
          label="Placement"
          selectedKey={placement}
          onSelectionChange={setPlacement}
        >
          <Item id="top">Top</Item>
          <Item id="bottom">Bottom</Item>
        </Select>
        <Switch isSelected={swipeable} onChange={setSwipeable}>
          Swipeable
        </Switch>
      </div>
    </div>
  );
}

Alert Dialog

Use the role="alertdialog" prop on the <Dialog> element to make an alert dialog. If the isDismissable prop is not explicitly set, the dialog will be not dismissable.

"use client";

import React from "react";
import { Button } from "@/registry/ui/default/core/button";
import {
  DialogRoot,
  Dialog,
  DialogFooter,
} from "@/registry/ui/default/core/dialog";

export default function Demo() {
  return (
    <DialogRoot>
      <Button variant="danger">Delete project</Button>
      <Dialog
        title="Delete project"
        description="Are you sure you want to delete this project? This action is permanent and cannot be undone."
        role="alertdialog"
        // isDissmissible={false}
      >
        {({ close }) => (
          <>
            <DialogFooter>
              <Button
                variant="outline"
                size={{ initial: "lg", sm: "md" }}
                onPress={close}
              >
                Cancel
              </Button>
              <Button
                variant="danger"
                size={{ initial: "lg", sm: "md" }}
                onPress={close}
              >
                Delete project
              </Button>
            </DialogFooter>
          </>
        )}
      </Dialog>
    </DialogRoot>
  );
}

Controlled

Use the isOpen and onOpenChange props to control the dialog's open state.

"use client";

import React from "react";
import { Button } from "@/registry/ui/default/core/button";
import {
  DialogRoot,
  Dialog,
  DialogFooter,
} from "@/registry/ui/default/core/dialog";
import { TextField } from "@/registry/ui/default/core/text-field";

export default function Demo() {
  return (
    <DialogRoot>
      <Button variant="outline">Edit Profile</Button>
      <Dialog title="Edit profile" description="Make changes to your profile.">
        {({ close }) => (
          <>
            <div className="space-y-4">
              <TextField autoFocus label="Name" defaultValue="Mehdi" />
              <TextField label="Username" defaultValue="@mehdibha_" />
            </div>
            <DialogFooter>
              <Button
                variant="outline"
                size={{ initial: "lg", sm: "md" }}
                onPress={close}
              >
                Cancel
              </Button>
              <Button
                variant="primary"
                size={{ initial: "lg", sm: "md" }}
                onPress={close}
              >
                Save changes
              </Button>
            </DialogFooter>
          </>
        )}
      </Dialog>
    </DialogRoot>
  );
}

Composition

If you need to customize things further, you can drop down to the composition level.

import React from "react";
import { Button } from "@/registry/ui/default/core/button";
import { DialogRoot, Dialog } from "@/registry/ui/default/core/dialog";
import { TextField } from "@/registry/ui/default/core/text-field";

export default function Demo() {
  return (
    <DialogRoot>
      <Button variant="outline">Edit username</Button>
      <Dialog title="Edit username">
        <TextField label="Username" defaultValue="@mehdibha_" />
      </Dialog>
    </DialogRoot>
  );
}

Examples

Async form submission

"use client";

import React from "react";
import { Button } from "@/registry/ui/default/core/button";
import {
  DialogRoot,
  Dialog,
  DialogFooter,
} from "@/registry/ui/default/core/dialog";
import { TextField } from "@/registry/ui/default/core/text-field";

export default function DialogDemo() {
  const [isPending, setIsPending] = React.useState(false);
  const handleSubmit = async () => {
    setIsPending(true);
    await new Promise((resolve) => setTimeout(resolve, 1000));
    setIsPending(false);
  };
  return (
    <DialogRoot>
      <Button variant="outline">Edit Profile</Button>
      <Dialog title="Edit profile" description="Make changes to your profile.">
        {({ close }) => (
          <>
            <div className="space-y-4">
              <TextField
                autoFocus
                label="Name"
                defaultValue="Mehdi"
                className="w-full"
              />
              <TextField label="Username" defaultValue="@mehdibha_" />
            </div>
            <DialogFooter>
              <Button
                variant="outline"
                size={{ initial: "lg", sm: "md" }}
                onPress={close}
              >
                Cancel
              </Button>
              <Button
                isPending={isPending}
                variant="primary"
                size={{ initial: "lg", sm: "md" }}
                onPress={async () => {
                  await handleSubmit();
                  close();
                }}
              >
                Save changes
              </Button>
            </DialogFooter>
          </>
        )}
      </Dialog>
    </DialogRoot>
  );
}

Nested dialog

Dialogs support nesting, allowing you to open a dialog from within another dialog.

Type
"use client";

import React from "react";
import { Button } from "@/registry/ui/default/core/button";
import { DialogRoot, Dialog } from "@/registry/ui/default/core/dialog";
import { Radio, RadioGroup } from "@/registry/ui/default/core/radio-group";

type Type = "modal" | "drawer" | "popover";

export default function Demo() {
  const [type, setType] = React.useState<Type>("modal");
  return (
    <div className="flex w-full items-center">
      <div className="flex flex-1 items-center justify-center">
        <DialogRoot>
          <Button variant="outline">Dialog</Button>
          <Dialog title="Dialog" type={type}>
            <DialogRoot>
              <Button variant="outline">Nested dialog</Button>
              <Dialog title="Nested dialog" type={type}></Dialog>
            </DialogRoot>
          </Dialog>
        </DialogRoot>
      </div>
      <div className="space-y-4 rounded-md border px-10 py-6">
        <RadioGroup
          label="Type"
          value={type}
          onChange={(value) => setType(value as Type)}
        >
          <Radio value="modal">Modal</Radio>
          <Radio value="drawer">Drawer</Radio>
          <Radio value="popover">Popover</Radio>
        </RadioGroup>
      </div>
    </div>
  );
}

API Reference

DialogRoot

PropTypeDefaultDescription
children*
ReactNode
-
isOpen
boolean
-
Whether the overlay is open by default (controlled).
defaultOpen
boolean
-
Whether the overlay is open by default (uncontrolled).
EventTypeDescription
onOpenChange
(isOpen: boolean) => void
Handler that is called when the overlay's open state changes.

Dialog

PropTypeDefaultDescription
children
ReactNode | (opts: DialogRenderProps) => ReactNode
-
Children of the dialog. A function may be provided to access a function to close the dialog.
className
string
-
The CSS className for the element.
style
CSSProperties
-
The inline style for the element.

Accessibility

Keyboard interactions

KeyDescription
Space Enter
When focus is on the trigger, opens the dialog.
Tab
Moves focus to the next button inside the dialog (last becomes first). If none of the buttons are selected, the focus is set on the first button.
Shift+Tab
Moves focus to the previous button inside the dialog (first becomes last). If none of the buttons are selected, the focus is set on the last button.
Esc
Dismisses the dialog and moves focus to the trigger.

Last updated on 10/11/2024