1. Components
  2. Overlays
  3. Dialog

Dialog

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

<DialogRoot>
  <Button prefix={<PenSquareIcon />}>Create issue</Button>
  <Dialog
    title="Create a new issue"
    description="Report an issue or create a feature request."
  >
    <DialogBody>
      <TextField aria-label="Title" placeholder="Title" autoFocus />
      <TextArea aria-label="Description" placeholder="description" />
    </DialogBody>
    <DialogFooter>
      <Button slot="close">Cancel</Button>
      <Button slot="close">Save changes</Button>
    </DialogFooter>
  </Dialog>
</DialogRoot>

Installation

npx dotui-cli@latest add dialog

Anatomy

A dialog consists of a container element and an optional title, description and close button. It can be placed within a Modal, Popover, Drawer or an Overlay to create modal dialogs, popovers, and other types of overlays. A DialogRoot can be used to open a dialog overlay in response to a user action, e.g. clicking a button.

import { Button } from "@/components/core/button";
import { DialogRoot, Dialog } from "@/components/core/dialog";
 
<DialogRoot>
  <Button>Open dialog</Button>
  <Dialog heading=".." description="..">
    {/* Dialog content */}
  </Dialog>
</DialogRoot>;
import { Button } from "@/components/core/button";
import {
  DialogRoot,
  DialogHeader
  DialogHeading,
  DialogDescription
  DialogContent,
  DialogFooter
} from "@/components/core/dialog";
import { Overlay } from "@/components/core/overlay";
 
<DialogRoot>
  <Button>Open dialog</Button>
  <Overlay type="modal">
    <DialogContent>
      <DialogHeader>
        <DialogHeading>{/* Dialog title */}</DialogHeading>
        <DialogDescription>{/* Dialog description */}</DialogDescription>
      </DialogHeader>
      <DialogBody>
      {/* Dialog content */}
      </DialogBody>
      <DialogFooter>
        <Button slot="close">Cancel</Button>
        <Button>Save changes</Button>
      </DialogFooter>
    </DialogContent>
  </Overlay>
</DialogRoot>;

Usage

Use dialog to grab the user's attention for critical tasks, like confirming actions, providing additional details, or capturing input, without navigating away from the current view.

Best practices

  • 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.

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>Create issue</Button>
  <Dialog type={type} mobileType={mobileType}>  {/* "modal", "popover", "drawer" */}
    {/* Dialog content */}
  </Dialog>
</DialogRoot>

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.

<DialogRoot>
  <Button>Delete project</Button>
  <Dialog role="alertdialog">
    {/* Dialog content */}
  </Dialog>
</DialogRoot>

Dismissable

Dialogs are dismissable by default (except alert dialogs) allowing users to click outside to close the dialog.

<DialogRoot>
  <Button>Create issue</Button>
  <Dialog isDismissable={isDismissable}>
    {/* Dialog content */}
  </Dialog>
</DialogRoot>

Inset content

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

<DialogRoot>
  <Button>Create issue</Button>
  <Dialog title=".." description="..">
    <DialogBody>
      <DialogInset>
        Content within the inset.
      </DialogInset>
      <p className="mt-4">Content outside the inset.</p>
    </DialogBody>
    {/* Dialog footer */}
  </Dialog>
</DialogRoot>

Controlled

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

const [isOpen, setOpen] = React.useState(false);
return (
  <DialogRoot isOpen={isOpen} onOpenChange={setOpen}>
    <Button>Open dialog</Button>
    <Dialog title="This is a heading" description="this is a description">
      content here
    </Dialog>
  </DialogRoot>
);

Composition

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

<DialogRoot>
  <Button>Create issue</Button>
  <Overlay>
    <DialogContent>
      <DialogHeader>
        <DialogHeading>Create a new issue</DialogHeading>
        <DialogDescription>
          Report an issue or create a feature request.
        </DialogDescription>
      </DialogHeader>
      <DialogBody>
        <TextField aria-label="Title" placeholder="Title" autoFocus />
        <TextArea aria-label="Description" placeholder="description" />
      </DialogBody>
      <DialogFooter>
        <Button>Cancel</Button>
        <Button>Save changes</Button>
      </DialogFooter>
    </DialogContent>
  </Overlay>
</DialogRoot>

Examples

Async form submission

"use client";

import React from "react";
import { Form } from "react-aria-components";
import { Button } from "@/components/dynamic-core/button";
import {
  DialogRoot,
  Dialog,
  DialogFooter,
  DialogBody,
} from "@/components/dynamic-core/dialog";
import { TextField } from "@/components/dynamic-core/text-field";

export default function DialogDemo() {
  const [isPending, setIsPending] = React.useState(false);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setIsPending(true);
    await new Promise((resolve) => setTimeout(resolve, 1000));
    setIsPending(false);
  };

  return (
    <DialogRoot>
      <Button>Edit username</Button>
      <Dialog title="Edit username" description="Make changes to your profile.">
        {({ close }) => (
          <Form
            onSubmit={(e) => {
              handleSubmit(e);
              close();
            }}
          >
            <DialogBody>
              <TextField
                autoFocus
                label="Username"
                defaultValue="@mehdibha_"
                isRequired
                className="w-full"
              />
            </DialogBody>
            <DialogFooter>
              <Button variant="outline" slot="close">
                Cancel
              </Button>
              <Button type="submit" isPending={isPending} variant="primary">
                Save changes
              </Button>
            </DialogFooter>
          </Form>
        )}
      </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 "@/components/dynamic-core/button";
import { DialogRoot, Dialog } from "@/components/dynamic-core/dialog";
import { Radio, RadioGroup } from "@/components/dynamic-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 gap-8">
      <DialogRoot>
        <Button variant="outline">Dialog</Button>
        <Dialog title="Dialog" type={type} mobileType={null}>
          <DialogRoot>
            <Button variant="outline">Nested dialog</Button>
            <Dialog
              title="Nested dialog"
              type={type}
              mobileType={null}
            ></Dialog>
          </DialogRoot>
        </Dialog>
      </DialogRoot>
      <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>
  );
}

API Reference

DialogRoot

PropTypeDefaultDescription
children*ReactNode-
isOpenboolean-Whether the overlay is open by default (controlled).
defaultOpenboolean-Whether the overlay is open by default (uncontrolled).

Dialog

PropTypeDefaultDescription
type'modal' |'drawer' |'popover' |'modal'The type of the overlay.
mobileType'modal' |'drawer' |'popover' |'drawer'The mobile type of the overlay.
isKeyboardDismissDisabledbooleanfalseWhether pressing the escape key to close the dialog should be disabled.
shouldCloseOnInteractOutside(element: Element) => boolean-When user interacts with the argument element outside of the overlay ref, return true if onClose should be called. This gives you a chance to filter out interaction with elements that should not dismiss the overlay. By default, onClose will always be called on interaction outside the overlay ref.
isOpenboolean-Whether the overlay is open by default (controlled).
defaultOpenboolean-Whether the overlay is open by default (uncontrolled).
modalPropsModalProps-Props applied to the Modal element. See ModalProps
drawerPropsDrawerProps-Props applied to the Drawer element. See DrawerProps
popoverPropsPopoverProps-Props applied to the Popover element. See PopoverProps
classNamestring-The CSS className for the element.
styleCSSProperties-The inline style for the element.
EventTypeDescription
onOpenChange(isOpen: boolean) => voidHandler that is called when the overlay's open state changes.
PropTypeDefaultDescription
role'dialog' | 'alertdialog''dialog'The accessibility role for the dialog.
idstring-The element's unique identifier. See MDN.
aria-labelstring-Defines a string value that labels the current element.
aria-labelledbystring-Identifies the element (or elements) that labels the current element.
aria-describedbystring-Identifies the element (or elements) that describes the object.
aria-detailsstring-Identifies the element (or elements) that provide a detailed, extended description for the object.

Accessibility

Keyboard interactions

KeyDescription
Space EnterWhen focus is on the trigger, opens the dialog.
TabMoves 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+TabMoves 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.
EscDismisses the dialog and moves focus to the trigger.
dotUI

Bringing singularity to the web.

Built by mehdibha. The source code is available on GitHub.