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>
</>
);
}
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>
</>
);
}
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:
"auto"
(default) - width is based on content"small"
- usesvar(--rmd-dialog-small-width)
and configured by core.$dialog-small-width or core.dialog-set-var(small-width, NEW_VALUE)"medium"
- usesvar(--rmd-dialog-medium-width)
and configured by core.$dialog-medium-width or core.dialog-set-var(medium-width, NEW_VALUE)"large"
- usesvar(--rmd-dialog-large-width)
and configured by core.$dialog-large-width or core.dialog-set-var(large-width, NEW_VALUE)"extra-large"
- usesvar(--rmd-dialog-extra-large-width)
and configured by core.$dialog-extra-large-width or core.dialog-set-var(extra-large-width, NEW_VALUE)
Modal Dialog
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>
</>
);
}
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:
- a new
fixedTo
ref prop is required to position itself to another element- the default anchor is set to
TOP_INNER_RIGHT_ANCHOR
- the default anchor is set to
- the CSS transition defaults to using a scale transition
- the overlay is hidden by default
- the page is scrollable while the dialog is visible by default
- the dialog will automatically hide if the user scrolls the dialog out of view
- the
fixedTo
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>
</>
);
}
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>
</>
);
}
Custom Transition
The Dialog
uses the useCSSTransition to handle
the visibility transitions and can be customized by providing the timeout
and
classNames
props.
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>
</>
);
}
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>
</>
);
}