Skip to main content
react-md

Dialog

Dialogs inform users about a task and can contain critical information, require decisions, or involve multiple tasks.

A dialog is a type of modal window that appears in front of app content to provide critical information or ask for a decision. Dialogs disable all app functionality when they appear, and remain on screen until confirmed, dismissed, or a required action has been taken.

Simple Example

A dialog can be created using the Dialog, DialogHeader, DialogTitle, DialogContent, and DialogFooter components. The Dialog is a controlled component requiring a visible state, onRequestClose function to close the dialog, and either an aria-label or aria-labelledby for accessibility.

A common pattern is to set the aria-labelledby to the DialogTitle since the title would normally describe the purpose of the dialog.

import { Button } from "@react-md/core/button/Button";
import { Dialog } from "@react-md/core/dialog/Dialog";
import { DialogContent } from "@react-md/core/dialog/DialogContent";
import { DialogFooter } from "@react-md/core/dialog/DialogFooter";
import { DialogHeader } from "@react-md/core/dialog/DialogHeader";
import { DialogTitle } from "@react-md/core/dialog/DialogTitle";
import { Typography } from "@react-md/core/typography/Typography";
import { useToggle } from "@react-md/core/useToggle";
import { type ReactElement, useId } from "react";

export default function SimpleExample(): ReactElement {
  const {
    toggled: visible,
    enable: showDialog,
    disable: hideDialog,
  } = useToggle(false);
  const titleId = useId();

  return (
    <>
      <Button onClick={showDialog}>Show</Button>
      <Dialog
        aria-labelledby={titleId}
        visible={visible}
        onRequestClose={hideDialog}
      >
        <DialogHeader>
          <DialogTitle id={titleId}>Simple Dialog</DialogTitle>
        </DialogHeader>
        <DialogContent>
          <Typography margin="bottom">
            This is some text in a dialog.
          </Typography>
        </DialogContent>
        <DialogFooter>
          <Button onClick={hideDialog}>Close</Button>
        </DialogFooter>
      </Dialog>
    </>
  );
}

Press Enter to start editing.

Full Page Dialog

A dialog can span the entire viewport by setting the type to "full-page".

import { AppBar } from "@react-md/core/app-bar/AppBar";
import { AppBarTitle } from "@react-md/core/app-bar/AppBarTitle";
import { Button } from "@react-md/core/button/Button";
import { Dialog } from "@react-md/core/dialog/Dialog";
import { DialogContent } from "@react-md/core/dialog/DialogContent";
import { Typography } from "@react-md/core/typography/Typography";
import { useToggle } from "@react-md/core/useToggle";
import CloseIcon from "@react-md/material-icons/CloseIcon";
import { type ReactElement, useId } from "react";

export default function FullPageDialogExample(): ReactElement {
  const titleId = useId();
  const {
    toggled: visible,
    enable: showDialog,
    disable: hideDialog,
  } = useToggle(false);

  return (
    <>
      <Button onClick={showDialog}>Show</Button>
      <Dialog
        aria-labelledby={titleId}
        type="full-page"
        visible={visible}
        onRequestClose={hideDialog}
      >
        <AppBar>
          <Button aria-label="Close" onClick={hideDialog} buttonType="icon">
            <CloseIcon />
          </Button>
          <AppBarTitle id={titleId}>Simple Full Page Dialog</AppBarTitle>
        </AppBar>
        <DialogContent>
          <Typography margin="none">This is some text in a dialog.</Typography>
        </DialogContent>
      </Dialog>
    </>
  );
}

Press Enter to start editing.

Dialog Widths

The Dialog can be updated to enforce a specific width instead of relying on the size of the content using the width prop. The available values are:

import { Button } from "@react-md/core/button/Button";
import { Dialog } from "@react-md/core/dialog/Dialog";
import { DialogContent } from "@react-md/core/dialog/DialogContent";
import { DialogHeader } from "@react-md/core/dialog/DialogHeader";
import { DialogTitle } from "@react-md/core/dialog/DialogTitle";
import { type DialogWidth } from "@react-md/core/dialog/styles";
import { Option } from "@react-md/core/form/Option";
import { Select } from "@react-md/core/form/Select";
import { Typography } from "@react-md/core/typography/Typography";
import { useToggle } from "@react-md/core/useToggle";
import { type ReactElement, useId, useState } from "react";

import styles from "./DialogWidths.module.scss";

export default function DialogWidths(): ReactElement {
  const { toggled, enable, disable } = useToggle();
  const titleId = useId();
  const [width, setWidth] = useState<DialogWidth>("auto");

  return (
    <>
      <Button onClick={enable}>Show</Button>
      <Dialog
        aria-labelledby={titleId}
        width={width}
        visible={toggled}
        onRequestClose={disable}
        className={styles.container}
      >
        <DialogHeader>
          <DialogTitle id={titleId}>Dialog Widths</DialogTitle>
        </DialogHeader>
        <DialogContent>
          <Select
            label="Width"
            inline
            value={width}
            onChange={(event) => setWidth(event.currentTarget.value)}
          >
            <Option value="auto">auto</Option>
            <Option value="small">small</Option>
            <Option value="medium">medium</Option>
            <Option value="large">large</Option>
            <Option value="extra-large">extra-large</Option>
          </Select>
          <Typography>
            Phasellus maximus non arcu sit amet tempus. Praesent eu pretium sem.
            Aliquam venenatis est sit amet turpis bibendum porta sit amet a
            tellus. Nulla a magna justo. Aliquam tempus eros in pulvinar
            condimentum. Etiam nec ipsum tincidunt, dapibus erat a, ullamcorper
            ipsum. Nam justo odio, iaculis ut justo id, varius facilisis massa.
            In vel sapien at metus laoreet fringilla. Sed condimentum ante at
            augue suscipit, vel sodales dui tempus. Etiam tincidunt leo rhoncus
            massa iaculis, at tristique sem pharetra. Etiam congue, leo ornare
            tincidunt tempor, urna ipsum aliquet urna, ut hendrerit elit turpis
            a metus. In tortor mauris, tincidunt vel tincidunt sagittis,
            vestibulum nec nibh. Aliquam erat volutpat. Quisque ac gravida urna.
          </Typography>
        </DialogContent>
      </Dialog>
    </>
  );
}

Press Enter to start editing.

@use "@react-md/core" with (
  $dialog-small-width: 25rem,
  $dialog-medium-width: 40rem,
  $dialog-large-width: 60rem,
  $dialog-extra-large-width: 80rem
);

.container {
  @include core.dialog-set-var(small-width, core.$dialog-small-width);
  @include core.dialog-set-var(medium-width, core.$dialog-medium-width);
  @include core.dialog-set-var(large-width, core.$dialog-large-width);
  @include core.dialog-set-var(
    extra-large-width,
    core.$dialog-extra-large-width
  );
}

Press Enter to start editing.

The default behavior for the dialog is to allow the user to click the overlay to close the dialog. If the user should be forced to interact with the modal to close it, enable the modal prop.

When the modal prop is enabled, the role should normally switch to an alertdialog to increase the accessibility. An aria-describedby should also be set on the dialog to describe the alert.

import { Button } from "@react-md/core/button/Button";
import { Dialog } from "@react-md/core/dialog/Dialog";
import { DialogContent } from "@react-md/core/dialog/DialogContent";
import { DialogFooter } from "@react-md/core/dialog/DialogFooter";
import { DialogHeader } from "@react-md/core/dialog/DialogHeader";
import { DialogTitle } from "@react-md/core/dialog/DialogTitle";
import { Typography } from "@react-md/core/typography/Typography";
import { useToggle } from "@react-md/core/useToggle";
import { type ReactElement, useId } from "react";

export default function ModalDialogExample(): ReactElement {
  const {
    toggled: visible,
    enable: showDialog,
    disable: hideDialog,
  } = useToggle(false);
  const titleId = useId();
  const descriptionId = useId();

  return (
    <>
      <Button onClick={showDialog}>Show</Button>
      <Dialog
        aria-labelledby={titleId}
        aria-describedby={descriptionId}
        role="alertdialog"
        modal
        visible={visible}
        onRequestClose={hideDialog}
      >
        <DialogHeader>
          <DialogTitle id={titleId}>
            Your session is about to expire
          </DialogTitle>
        </DialogHeader>
        <DialogContent>
          <Typography id={descriptionId} margin="none">
            To extend your session, click the OK button
          </Typography>
        </DialogContent>
        <DialogFooter>
          <Button onClick={hideDialog}>OK</Button>
        </DialogFooter>
      </Dialog>
    </>
  );
}

Press Enter to start editing.

Fixed Dialog

If a dialog should be positioned relative to another element, use the FixedDialog component. The FixedDialog component maintains the same API as the Dialog component with a few changes:

"use client";

import { Button } from "@react-md/core/button/Button";
import { DialogContent } from "@react-md/core/dialog/DialogContent";
import { DialogFooter } from "@react-md/core/dialog/DialogFooter";
import { DialogHeader } from "@react-md/core/dialog/DialogHeader";
import { DialogTitle } from "@react-md/core/dialog/DialogTitle";
import { FixedDialog } from "@react-md/core/dialog/FixedDialog";
import { Typography } from "@react-md/core/typography/Typography";
import { useToggle } from "@react-md/core/useToggle";
import { type ReactElement, useId, useRef } from "react";

export default function FixedDialogExample(): ReactElement {
  const {
    toggled: visible,
    enable: showDialog,
    disable: hideDialog,
  } = useToggle(false);
  const fixedTo = useRef<HTMLButtonElement>(null);
  const titleId = useId();

  return (
    <>
      <Button ref={fixedTo} onClick={showDialog}>
        Show
      </Button>
      <FixedDialog
        aria-labelledby={titleId}
        fixedTo={fixedTo}
        visible={visible}
        onRequestClose={hideDialog}
      >
        <DialogHeader>
          <DialogTitle id={titleId}>Hello, world!</DialogTitle>
        </DialogHeader>
        <DialogContent>
          <Typography margin="none">Additional content</Typography>
        </DialogContent>
        <DialogFooter>
          <Button onClick={hideDialog}>OK</Button>
        </DialogFooter>
      </FixedDialog>
    </>
  );
}

Press Enter to start editing.

Setting the Initial Focus

When the dialog gains visibility, the current focus will move to the dialog itself and trap focus within the dialog. If another element in the dialog should gain focus instead, enable the autoFocus prop on that element.

import { Button } from "@react-md/core/button/Button";
import { Dialog } from "@react-md/core/dialog/Dialog";
import { DialogContent } from "@react-md/core/dialog/DialogContent";
import { DialogFooter } from "@react-md/core/dialog/DialogFooter";
import { DialogHeader } from "@react-md/core/dialog/DialogHeader";
import { DialogTitle } from "@react-md/core/dialog/DialogTitle";
import { Typography } from "@react-md/core/typography/Typography";
import { useToggle } from "@react-md/core/useToggle";
import { type ReactElement, useId } from "react";

export default function SettingInitialFocusExample(): ReactElement {
  const titleId = useId();
  const {
    toggled: visible,
    enable: showDialog,
    disable: hideDialog,
  } = useToggle(false);

  return (
    <>
      <Button onClick={showDialog}>Show</Button>
      <Dialog
        visible={visible}
        onRequestClose={hideDialog}
        aria-labelledby={titleId}
      >
        <DialogHeader>
          <DialogTitle id={titleId}>Simple Dialog</DialogTitle>
        </DialogHeader>
        <DialogContent>
          <Typography margin="bottom">
            This is some text in a dialog.
          </Typography>
        </DialogContent>
        <DialogFooter>
          <Button autoFocus onClick={hideDialog}>
            Close
          </Button>
        </DialogFooter>
      </Dialog>
    </>
  );
}

Press Enter to start editing.

Custom Transition

The Dialog uses the useCSSTransition to handle the visibility transitions and can be customized by providing the timeout and classNames props.

import { Button } from "@react-md/core/button/Button";
import { Dialog } from "@react-md/core/dialog/Dialog";
import { DialogContent } from "@react-md/core/dialog/DialogContent";
import { DialogFooter } from "@react-md/core/dialog/DialogFooter";
import { DialogHeader } from "@react-md/core/dialog/DialogHeader";
import { DialogTitle } from "@react-md/core/dialog/DialogTitle";
import { Typography } from "@react-md/core/typography/Typography";
import { useToggle } from "@react-md/core/useToggle";
import { type ReactElement, useId } from "react";

import styles from "./CustomTransitionExample.module.scss";

export default function CustomTransitionExample(): ReactElement {
  const {
    toggled: visible,
    enable: showDialog,
    disable: hideDialog,
  } = useToggle(false);
  const titleId = useId();

  return (
    <>
      <Button onClick={showDialog}>Show</Button>
      <Dialog
        aria-labelledby={titleId}
        visible={visible}
        onRequestClose={hideDialog}
        className={styles.dialog}
        classNames={{
          enter: styles.enter,
          enterActive: styles.enterActive,
          exit: styles.exit,
          exitActive: styles.exitActive,
        }}
      >
        <DialogHeader>
          <DialogTitle id={titleId}>Simple Dialog</DialogTitle>
        </DialogHeader>
        <DialogContent>
          <Typography margin="bottom">
            This is some text in a dialog.
          </Typography>
        </DialogContent>
        <DialogFooter>
          <Button onClick={hideDialog}>Close</Button>
        </DialogFooter>
      </Dialog>
    </>
  );
}

Press Enter to start editing.

@use "everything";

.enter {
  opacity: 0;
}

.enterActive {
  opacity: 1;
  transition: opacity everything.$enter-duration
    everything.$deceleration-timing-function;
}

.exit {
  opacity: 1;
}

.exitActive {
  opacity: 0;
  transition: opacity everything.$enter-duration
    everything.$acceleration-timing-function;
}

.dialog {
  width: 30rem;
}

Press Enter to start editing.

Nested Dialogs

Dialogs can be nested without any additional setup since the dialogs are portalled behind the scenes and only the topmost overlay will be shown at a time.

If multiple overlays were shown at the same time, the overlay would become darker as more dialogs are shown which can cause performance issues on mobile devices.

"use client";

import { Button } from "@react-md/core/button/Button";
import { Dialog } from "@react-md/core/dialog/Dialog";
import { DialogContent } from "@react-md/core/dialog/DialogContent";
import { DialogFooter } from "@react-md/core/dialog/DialogFooter";
import { DialogHeader } from "@react-md/core/dialog/DialogHeader";
import { DialogTitle } from "@react-md/core/dialog/DialogTitle";
import { useToggle } from "@react-md/core/useToggle";
import { type ReactElement } from "react";

export default function NestedDialogsExample(): ReactElement {
  const { toggle, toggled } = useToggle();
  return <InfiniteDialog key={`${toggled}`} depth={0} closeAll={toggle} />;
}

interface InfiniteDialogProps {
  depth: number;
  closeAll: () => void;
}

function InfiniteDialog(props: InfiniteDialogProps): ReactElement {
  const { depth, closeAll } = props;
  const { enable: show, disable: hide, toggled: visible } = useToggle();

  return (
    <>
      <Button onClick={show}>Show</Button>
      <Dialog aria-label="Dialog" visible={visible} onRequestClose={hide}>
        <DialogHeader>
          <DialogTitle>Dialog Depth {depth}</DialogTitle>
        </DialogHeader>
        <DialogContent>
          <InfiniteDialog depth={depth + 1} closeAll={closeAll} />
        </DialogContent>
        <DialogFooter>
          <Button theme="error" onClick={closeAll}>
            Close All
          </Button>
        </DialogFooter>
      </Dialog>
    </>
  );
}

Press Enter to start editing.

Nested Dialogs Default Visible

If nested dialogs should be visible by on initial page load or mount, trigger the show behavior into a useEffect instead of setting the initial state to true. If multiple dialogs are rendered at once, the topmost dialog will be shown instead of the child dialogs due to how React renders.

"use client";

import { Button } from "@react-md/core/button/Button";
import { Dialog } from "@react-md/core/dialog/Dialog";
import { DialogContent } from "@react-md/core/dialog/DialogContent";
import { DialogFooter } from "@react-md/core/dialog/DialogFooter";
import { DialogHeader } from "@react-md/core/dialog/DialogHeader";
import { DialogTitle } from "@react-md/core/dialog/DialogTitle";
import { useToggle } from "@react-md/core/useToggle";
import { type ReactElement, useEffect } from "react";

export default function NestedDialogsVisibleExample(): ReactElement {
  const { toggle, toggled } = useToggle();
  return <InfiniteDialog key={`${toggled}`} depth={0} closeAll={toggle} />;
}

interface InfiniteDialogProps {
  depth: number;
  closeAll: () => void;
}

function InfiniteDialog(props: InfiniteDialogProps): ReactElement {
  const { depth, closeAll } = props;

  const defaultVisible = depth > 0 && depth < 3;
  // try setting `useToggle(defaultVisible)` to see the difference
  const { enable: show, disable: hide, toggled: visible } = useToggle();
  useEffect(() => {
    if (defaultVisible) {
      show();
    }
  }, [defaultVisible, show]);

  return (
    <>
      <Button onClick={show}>Show</Button>
      <Dialog aria-label="Dialog" visible={visible} onRequestClose={hide}>
        <DialogHeader>
          <DialogTitle>Dialog Depth {depth}</DialogTitle>
        </DialogHeader>
        <DialogContent>
          <InfiniteDialog depth={depth + 1} closeAll={closeAll} />
        </DialogContent>
        <DialogFooter>
          <Button theme="error" onClick={closeAll}>
            Close All
          </Button>
        </DialogFooter>
      </Dialog>
    </>
  );
}

Press Enter to start editing.