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.
Dialogs are purposefully interruptive, so they should be used sparingly.
Dialogs are implemented using a collection of related components:
<DialogContent />
.{{ādemoā: āSimpleDialogDemo.jsā}}
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
Alerts are urgent interruptions, requiring acknowledgement, that inform the user about a situation.
Most alerts donāt need titles. They summarize a decision in a sentence or two by either:
Use title bar alerts only for high-risk situations, such as the potential loss of connectivity. Users should be able to understand the choices based on the title and button text alone.
If a title is required:
{{ādemoā: āAlertDialog.jsā}}
You can also swap out the transition, the next example uses Slide
.
{{ādemoā: āAlertDialogSlide.jsā}}
Form dialogs allow users to fill out form fields within a dialog. For example, if your site prompts for potential subscribers to fill in their email address, they can fill out the email field and touch āSubmitā.
{{ādemoā: āFormDialog.jsā}}
Here is an example of customizing the component. You can learn more about this in the overrides documentation page.
The dialog has a close button added to aid usability.
{{ādemoā: āCustomizedDialogs.jsā}}
{{ādemoā: āFullScreenDialog.jsā}}
You can set a dialog maximum width by using the maxWidth
enumerable in combination with the fullWidth
boolean.
When the fullWidth
prop is true, the dialog will adapt based on the maxWidth
value.
{{ādemoā: āMaxWidthDialog.jsā}}
You may make a dialog responsively full screen using useMediaQuery
.
import useMediaQuery from '@mui/material/useMediaQuery';
function MyComponent() {
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down('md'));
return <Dialog fullScreen={fullScreen} />;
}
{{ādemoā: āResponsiveDialog.jsā}}
Confirmation dialogs require users to explicitly confirm their choice before an option is committed. For example, users can listen to multiple ringtones but only make a final selection upon touching āOKā.
Touching āCancelā in a confirmation dialog, cancels the action, discards any changes, and closes the dialog.
{{ādemoā: āConfirmationDialog.jsā}}
Dialogs can also be non-modal, meaning they donāt interrupt user interaction behind it. Visit the Nielsen Norman Group article for more in-depth guidance about modal vs. non-modal dialog usage.
The demo below shows a persistent cookie banner, a common non-modal dialog use case.
import * as React from 'react';
import Stack from '@mui/material/Stack';
import TrapFocus from '@mui/material/Unstable_TrapFocus';
import CssBaseline from '@mui/material/CssBaseline';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Container from '@mui/material/Container';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
import Paper from '@mui/material/Paper';
import Fade from '@mui/material/Fade';
import Button from '@mui/material/Button';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
export default function CookiesBanner() {
const [bannerOpen, setBannerOpen] = React.useState(true);
const closeBanner = () => {
setBannerOpen(false);
};
return (
<React.Fragment>
<CssBaseline />
<AppBar position="fixed" component="nav">
<Toolbar>
<IconButton size="large" edge="start" color="inherit" aria-label="menu">
<MenuIcon />
</IconButton>
</Toolbar>
</AppBar>
<Container component="main" sx={{ pt: 3 }}>
<Toolbar />
<Typography sx={{ marginBottom: 2 }}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Rhoncus dolor purus non
enim praesent elementum facilisis leo vel. Risus at ultrices mi tempus
imperdiet.
</Typography>
<Typography sx={{ marginBottom: 2 }}>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Rhoncus dolor purus non
enim praesent elementum facilisis leo vel. Risus at ultrices mi tempus
imperdiet.
</Typography>
</Container>
<TrapFocus open disableAutoFocus disableEnforceFocus>
<Fade appear={false} in={bannerOpen}>
<Paper
role="dialog"
aria-modal="false"
aria-label="Cookie banner"
square
variant="outlined"
tabIndex={-1}
sx={{
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
m: 0,
p: 2,
borderWidth: 0,
borderTopWidth: 1,
}}
>
<Stack
direction={{ xs: 'column', sm: 'row' }}
sx={{ justifyContent: 'space-between', gap: 2 }}
>
<Box
sx={{ flexShrink: 1, alignSelf: { xs: 'flex-start', sm: 'center' } }}
>
<Typography sx={{ fontWeight: 'bold' }}>
This website uses cookies
</Typography>
<Typography variant="body2">
example.com relies on cookies to improve your experience.
</Typography>
</Box>
<Stack
direction={{
xs: 'row-reverse',
sm: 'row',
}}
sx={{
gap: 2,
flexShrink: 0,
alignSelf: { xs: 'flex-end', sm: 'center' },
}}
>
<Button size="small" onClick={closeBanner} variant="contained">
Allow all
</Button>
<Button size="small" onClick={closeBanner}>
Reject all
</Button>
</Stack>
</Stack>
</Paper>
</Fade>
</TrapFocus>
</React.Fragment>
);
}
You can create a draggable dialog by using react-draggable.
To do so, you can pass the imported Draggable
component as the PaperComponent
of the Dialog
component.
This will make the entire dialog draggable.
{{ādemoā: āDraggableDialog.jsā}}
When dialogs become too long for the userās viewport or device, they scroll.
scroll=paper
the content of the dialog scrolls within the paper element.scroll=body
the content of the dialog scrolls within the body element.Try the demo below to see what we mean:
{{ādemoā: āScrollDialog.jsā}}
Follow the Modal performance section.
Follow the Modal limitations section.
For more advanced use cases you might be able to take advantage of:
The package material-ui-confirm
provides dialogs for confirming user actions without writing boilerplate code.
Follow the Modal accessibility section.
You can create and manipulate dialogs imperatively with the useDialogs()
API in @toolpad/core
. This hook handles
window.alert()
, window.confirm()
and window.prompt()
The following example demonstrates some of these features:
import * as React from 'react';
import { DialogsProvider, useDialogs, DialogProps } from '@toolpad/core/useDialogs';
import Button from '@mui/material/Button';
import LoadingButton from '@mui/lab/LoadingButton';
import Dialog from '@mui/material/Dialog';
import Alert from '@mui/material/Alert';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
interface DeleteError {
id: string | null;
error: string | null;
}
function MyCustomDialog({ open, onClose, payload }: DialogProps<DeleteError>) {
return (
<Dialog fullWidth open={open} onClose={() => onClose()}>
<DialogTitle>Custom Error Handler</DialogTitle>
<DialogContent>
<Alert severity="error">
{`An error occurred while deleting item "${payload.id}":`}
<pre>{payload.error}</pre>
</Alert>
</DialogContent>
<DialogActions>
<Button onClick={() => onClose()}>Close me</Button>
</DialogActions>
</Dialog>
);
}
const mockApiDelete = async (id: string | null) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!id) {
reject(new Error('ID is required'));
} else if (parseInt(id, 10) % 2 === 0) {
console.log('id', parseInt(id, 10));
resolve(true);
} else if (parseInt(id, 10) % 2 === 1) {
reject(new Error('Can not delete odd numbered elements'));
} else if (Number.isNaN(parseInt(id, 10))) {
reject(new Error('ID must be a number'));
} else {
reject(new Error('Unknown error'));
}
}, 1000);
});
};
function DemoContent() {
const dialogs = useDialogs();
const [isDeleting, setIsDeleting] = React.useState(false);
const handleDelete = async () => {
const id = await dialogs.prompt('Enter the ID to delete', {
okText: 'Delete',
cancelText: 'Cancel',
});
if (id) {
const deleteConfirmed = await dialogs.confirm(
`Are you sure you want to delete "${id}"?`,
);
if (deleteConfirmed) {
try {
setIsDeleting(true);
await mockApiDelete(id);
dialogs.alert('Deleted!');
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
await dialogs.open(MyCustomDialog, { id, error: message });
} finally {
setIsDeleting(false);
}
}
}
};
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', gap: 16 }}>
<LoadingButton
variant="contained"
loading={isDeleting}
onClick={handleDelete}
>
Delete
</LoadingButton>
</div>
</div>
);
}
export default function ToolpadDialogsNoSnap() {
return (
<DialogsProvider>
<DemoContent />
</DialogsProvider>
);
}
const handleDelete = async () => {
const id = await dialogs.prompt('Enter the ID to delete', {
okText: 'Delete',
cancelText: 'Cancel',
});
if (id) {
const deleteConfirmed = await dialogs.confirm(
`Are you sure you want to delete "${id}"?`,
);
if (deleteConfirmed) {
try {
setIsDeleting(true);
await mockApiDelete(id);
dialogs.alert('Deleted!');
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
await dialogs.open(MyCustomDialog, { id, error: message });
} finally {
setIsDeleting(false);
}
}
}
};