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.
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>
</>
);
}
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>
</>
);
}
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:
"auto" (default) - width is based on content"small" - uses var(--rmd-dialog-small-width) and configured by
core.$dialog-small-width or core.dialog-set-var(small-width, NEW_VALUE)"medium" - uses var(--rmd-dialog-medium-width) and configured by
core.$dialog-medium-width or core.dialog-set-var(medium-width, NEW_VALUE)"large" - uses var(--rmd-dialog-large-width) and configured by
core.$dialog-large-width or core.dialog-set-var(large-width, NEW_VALUE)"extra-large" - uses var(--rmd-dialog-extra-large-width) and configured
by core.$dialog-extra-large-width or core.dialog-set-var(extra-large-width, NEW_VALUE)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>
</>
);
}
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:
fixedTo ref prop is required to position itself to another element
TOP_INNER_RIGHT_ANCHORfixedTo element is not re-focused for this flow"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>
</>
);
}
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>
</>
);
}
The Dialog uses the useCSSTransition to handle
the visibility transitions and can be customized by providing the timeout and
classNames props.
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>
</>
);
}
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>
</>
);
}