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.
Dialogs are purposefully interruptive, so they should be used sparingly.
How the live demos on this page work. In React, a
dialog’s open state lives in a useState()
hook. muiMaterial has no hooks, so these demos keep that state in the
URL with reactRouter: each
dialog is the element of a route and is rendered with
open = TRUE.
-
Open: a button links to the dialog’s route, for
example
Button(href = "#/alert-dialog"). -
Close from a button: the button links back home,
for example
Button(href = "#/"). -
Close on backdrop click or Escape:
onCloseresets the URL hash (thecloseDialoghelper below).
Everything runs in the browser, so the demos work in this static page, in Quarto documents and in Shiny apps. A bonus is that the dialogs become deep links, and the browser Back button closes them. For server-driven dialogs, see Dialogs in Shiny apps at the end of this page.
# Used by every demo below: close the dialog by navigating back to "#/"
closeDialog <- JS("() => { window.location.hash = '/'; }")Introduction
Dialogs are implemented using a collection of related components:
-
Dialog(): the parent component that renders the modal. -
DialogTitle(): a wrapper used for the title of a Dialog. -
DialogActions(): an optional container for a Dialog’s Buttons. -
DialogContent(): an optional container for displaying the Dialog’s content. -
DialogContentText(): a wrapper for text inside ofDialogContent(). -
Slide(): optional Transition used to slide the Dialog in from the edge of the screen.
The selected account is stored as a URL query parameter
(#/?backup=...) and read back with
useSearchParams().
emails <- c("[email protected]", "[email protected]")
muiMaterialPage(
useMaterialIconsFilled = TRUE,
CssBaseline(),
RouterProvider(
router = createHashRouter(
Route(
path = "/",
element = Box(
Typography(
variant = "subtitle1",
component = "div",
"Selected: ",
useSearchParams(
param = "backup",
render = JS("v => v.length ? v[0] : '[email protected]'")
)
),
tags$br(),
Button(variant = "outlined", href = "#/simple-dialog", "Open simple dialog"),
Outlet()
),
Route(
path = "simple-dialog",
element = Dialog(
open = TRUE,
onClose = JS("() => { window.history.back(); }"),
DialogTitle("Set backup account"),
List(
sx = list(pt = 0),
lapply(emails, function(email) {
ListItem(
disablePadding = TRUE,
ListItemButton(
href = paste0("#/?backup=", email),
ListItemAvatar(
# blue[100] and blue[600] from the MUI color palette
Avatar(sx = list(bgcolor = "#bbdefb", color = "#1e88e5"), Icon("person"))
),
ListItemText(primary = email)
)
)
}),
ListItem(
disablePadding = TRUE,
ListItemButton(
autoFocus = TRUE,
href = "#/?backup=addAccount",
ListItemAvatar(Avatar(Icon("add"))),
ListItemText(primary = "Add account")
)
)
)
)
),
Route(path = "*", element = NULL)
)
)
)
)JS code
import * as React from 'react';
import PropTypes from 'prop-types';
import Button from '@mui/material/Button';
import Avatar from '@mui/material/Avatar';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemAvatar from '@mui/material/ListItemAvatar';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import DialogTitle from '@mui/material/DialogTitle';
import Dialog from '@mui/material/Dialog';
import PersonIcon from '@mui/icons-material/Person';
import AddIcon from '@mui/icons-material/Add';
import Typography from '@mui/material/Typography';
import { blue } from '@mui/material/colors';
const emails = ['[email protected]', '[email protected]'];
function SimpleDialog(props) {
const { onClose, selectedValue, open } = props;
const handleClose = () => {
onClose(selectedValue);
};
const handleListItemClick = (value) => {
onClose(value);
};
return (
<Dialog onClose={handleClose} open={open}>
<DialogTitle>Set backup account</DialogTitle>
<List sx={{ pt: 0 }}>
{emails.map((email) => (
<ListItem disablePadding key={email}>
<ListItemButton onClick={() => handleListItemClick(email)}>
<ListItemAvatar>
<Avatar sx={{ bgcolor: blue[100], color: blue[600] }}>
<PersonIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary={email} />
</ListItemButton>
</ListItem>
))}
<ListItem disablePadding>
<ListItemButton
autoFocus
onClick={() => handleListItemClick('addAccount')}
>
<ListItemAvatar>
<Avatar>
<AddIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Add account" />
</ListItemButton>
</ListItem>
</List>
</Dialog>
);
}
SimpleDialog.propTypes = {
onClose: PropTypes.func.isRequired,
open: PropTypes.bool.isRequired,
selectedValue: PropTypes.string.isRequired,
};
export default function SimpleDialogDemo() {
const [open, setOpen] = React.useState(false);
const [selectedValue, setSelectedValue] = React.useState(emails[1]);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = (value) => {
setOpen(false);
setSelectedValue(value);
};
return (
<div>
<Typography variant="subtitle1" component="div">
Selected: {selectedValue}
</Typography>
<br />
<Button variant="outlined" onClick={handleClickOpen}>
Open simple dialog
</Button>
<SimpleDialog
selectedValue={selectedValue}
open={open}
onClose={handleClose}
/>
</div>
);
}Basics
library(muiMaterial)
Dialog(open = TRUE, DialogTitle("Title"))Alerts
Alerts are urgent interruptions, requiring acknowledgement, that inform the user about a situation.
Use role = "alertdialog" to create an Alert Dialog. This
provides assistive technologies the correct purpose of the Dialog.
Most alerts don’t need titles. They summarize a decision in a sentence or two by either:
- Asking a question (for example “Delete this conversation?”)
- Making a statement related to the action buttons
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:
- Use a clear question or statement with an explanation in the content area, such as “Erase USB storage?”.
- Avoid apologies, ambiguity, or questions, such as “Warning!” or “Are you sure?”
muiMaterialPage(
CssBaseline(),
Button(variant = "outlined", href = "#/alert-dialog", "Open alert dialog"),
RouterProvider(
router = createHashRouter(
Route(
path = "alert-dialog",
element = Dialog(
open = TRUE,
onClose = closeDialog,
`aria-labelledby` = "alert-dialog-title",
`aria-describedby` = "alert-dialog-description",
role = "alertdialog",
DialogTitle(id = "alert-dialog-title", "Use Google's location service?"),
DialogContent(
DialogContentText(
id = "alert-dialog-description",
"Let Google help apps determine location. This means sending anonymous
location data to Google, even when no apps are running."
)
),
DialogActions(
Button(href = "#/", autoFocus = TRUE, "Disagree"),
Button(href = "#/", "Agree")
)
)
),
Route(path = "*", element = NULL)
)
)
)JS code
import * as React from 'react';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
export default function AlertDialog() {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<React.Fragment>
<Button variant="outlined" onClick={handleClickOpen}>
Open alert dialog
</Button>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
role="alertdialog"
>
<DialogTitle id="alert-dialog-title">
{"Use Google's location service?"}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Let Google help apps determine location. This means sending anonymous
location data to Google, even when no apps are running.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} autoFocus>
Disagree
</Button>
<Button onClick={handleClose}>Agree</Button>
</DialogActions>
</Dialog>
</React.Fragment>
);
}Transitions
You can swap out the default transition with the
slots$transition and slotProps$transition
props. The next example uses Slide. A component (rather
than an element) is expected here, so it is passed as a JavaScript
reference with JS().
muiMaterialPage(
CssBaseline(),
Button(variant = "outlined", href = "#/alert-dialog-slide", "Slide in alert dialog"),
RouterProvider(
router = createHashRouter(
Route(
path = "alert-dialog-slide",
element = Dialog(
open = TRUE,
slots = list(transition = JS("jsmodule['@mui/material'].Slide")),
slotProps = list(transition = list(direction = "up")),
keepMounted = TRUE,
onClose = closeDialog,
`aria-describedby` = "alert-dialog-slide-description",
role = "alertdialog",
DialogTitle("Use Google's location service?"),
DialogContent(
DialogContentText(
id = "alert-dialog-slide-description",
"Let Google help apps determine location. This means sending anonymous
location data to Google, even when no apps are running."
)
),
DialogActions(
Button(href = "#/", autoFocus = TRUE, "Disagree"),
Button(href = "#/", "Agree")
)
)
),
Route(path = "*", element = NULL)
)
)
)JS code
import * as React from 'react';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import Slide from '@mui/material/Slide';
const Transition = React.forwardRef(function Transition(props, ref) {
return <Slide direction="up" ref={ref} {...props} />;
});
export default function AlertDialogSlide() {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<React.Fragment>
<Button variant="outlined" onClick={handleClickOpen}>
Slide in alert dialog
</Button>
<Dialog
open={open}
slots={{
transition: Transition,
}}
keepMounted
onClose={handleClose}
aria-describedby="alert-dialog-slide-description"
role="alertdialog"
>
<DialogTitle>{"Use Google's location service?"}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-slide-description">
Let Google help apps determine location. This means sending anonymous
location data to Google, even when no apps are running.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} autoFocus>
Disagree
</Button>
<Button onClick={handleClose}>Agree</Button>
</DialogActions>
</Dialog>
</React.Fragment>
);
}Form dialogs
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’.
The form’s onSubmit handler is a JS()
function: it reads the email, logs it to the browser console and closes
the dialog. In a Shiny app you would instead send the value to the
server, for example with
Shiny.setInputValue('email', email).
muiMaterialPage(
CssBaseline(),
Button(variant = "outlined", href = "#/form-dialog", "Open form dialog"),
RouterProvider(
router = createHashRouter(
Route(
path = "form-dialog",
element = Dialog(
open = TRUE,
onClose = closeDialog,
DialogTitle("Subscribe"),
DialogContent(
DialogContentText(
"To subscribe to this website, please enter your email address here. We
will send updates occasionally."
),
Box(
component = "form",
id = "subscription-form",
onSubmit = JS(
"(event) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
console.log(formData.get('email'));
window.location.hash = '/';
}"
),
TextField(
autoFocus = TRUE,
required = TRUE,
margin = "dense",
id = "name",
name = "email",
label = "Email Address",
type = "email",
fullWidth = TRUE,
variant = "standard"
)
)
),
DialogActions(
Button(href = "#/", "Cancel"),
Button(type = "submit", form = "subscription-form", "Subscribe")
)
)
),
Route(path = "*", element = NULL)
)
)
)JS code
import * as React from 'react';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
export default function FormDialog() {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const handleSubmit = (event) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const formJson = Object.fromEntries(formData.entries());
const email = formJson.email;
console.log(email);
handleClose();
};
return (
<React.Fragment>
<Button variant="outlined" onClick={handleClickOpen}>
Open form dialog
</Button>
<Dialog open={open} onClose={handleClose}>
<DialogTitle>Subscribe</DialogTitle>
<DialogContent>
<DialogContentText>
To subscribe to this website, please enter your email address here. We
will send updates occasionally.
</DialogContentText>
<form onSubmit={handleSubmit} id="subscription-form">
<TextField
autoFocus
required
margin="dense"
id="name"
name="email"
label="Email Address"
type="email"
fullWidth
variant="standard"
/>
</form>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button type="submit" form="subscription-form">
Subscribe
</Button>
</DialogActions>
</Dialog>
</React.Fragment>
);
}Customization
Here is an example of customizing the component. You can learn more
about this in the overrides
documentation page. In R, the styled() API of the
original demo becomes nested selectors in the sx
argument.
The dialog has a close button added to aid usability.
muiMaterialPage(
useMaterialIconsFilled = TRUE,
CssBaseline(),
Button(variant = "outlined", href = "#/customized-dialog", "Open dialog"),
RouterProvider(
router = createHashRouter(
Route(
path = "customized-dialog",
element = Dialog(
open = TRUE,
onClose = closeDialog,
`aria-labelledby` = "customized-dialog-title",
sx = list(
"& .MuiDialogContent-root" = list(padding = 2),
"& .MuiDialogActions-root" = list(padding = 1)
),
DialogTitle(sx = list(m = 0, p = 2), id = "customized-dialog-title", "Modal title"),
IconButton(
`aria-label` = "close",
href = "#/",
sx = list(position = "absolute", right = 8, top = 8, color = "grey.500"),
Icon("close")
),
DialogContent(
dividers = TRUE,
Typography(
gutterBottom = TRUE,
"Cras mattis consectetur purus sit amet fermentum. Cras justo odio,
dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac
consectetur ac, vestibulum at eros."
),
Typography(
gutterBottom = TRUE,
"Praesent commodo cursus magna, vel scelerisque nisl consectetur et.
Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor."
),
Typography(
gutterBottom = TRUE,
"Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus
magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec
ullamcorper nulla non metus auctor fringilla."
)
),
DialogActions(
Button(autoFocus = TRUE, href = "#/", "Save changes")
)
)
),
Route(path = "*", element = NULL)
)
)
)JS code
import * as React from 'react';
import Button from '@mui/material/Button';
import { styled } from '@mui/material/styles';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import IconButton from '@mui/material/IconButton';
import CloseIcon from '@mui/icons-material/Close';
import Typography from '@mui/material/Typography';
const BootstrapDialog = styled(Dialog)(({ theme }) => ({
'& .MuiDialogContent-root': {
padding: theme.spacing(2),
},
'& .MuiDialogActions-root': {
padding: theme.spacing(1),
},
}));
export default function CustomizedDialogs() {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<React.Fragment>
<Button variant="outlined" onClick={handleClickOpen}>
Open dialog
</Button>
<BootstrapDialog
onClose={handleClose}
aria-labelledby="customized-dialog-title"
open={open}
>
<DialogTitle sx={{ m: 0, p: 2 }} id="customized-dialog-title">
Modal title
</DialogTitle>
<IconButton
aria-label="close"
onClick={handleClose}
sx={(theme) => ({
position: 'absolute',
right: 8,
top: 8,
color: theme.palette.grey[500],
})}
>
<CloseIcon />
</IconButton>
<DialogContent dividers>
<Typography gutterBottom>
Cras mattis consectetur purus sit amet fermentum. Cras justo odio,
dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac
consectetur ac, vestibulum at eros.
</Typography>
<Typography gutterBottom>
Praesent commodo cursus magna, vel scelerisque nisl consectetur et.
Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.
</Typography>
<Typography gutterBottom>
Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus
magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec
ullamcorper nulla non metus auctor fringilla.
</Typography>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleClose}>
Save changes
</Button>
</DialogActions>
</BootstrapDialog>
</React.Fragment>
);
}Full-screen dialogs
muiMaterialPage(
useMaterialIconsFilled = TRUE,
CssBaseline(),
Button(variant = "outlined", href = "#/full-screen-dialog", "Open full-screen dialog"),
RouterProvider(
router = createHashRouter(
Route(
path = "full-screen-dialog",
element = Dialog(
fullScreen = TRUE,
open = TRUE,
onClose = closeDialog,
slots = list(transition = JS("jsmodule['@mui/material'].Slide")),
slotProps = list(transition = list(direction = "up")),
AppBar(
sx = list(position = "relative"),
Toolbar(
IconButton(
edge = "start",
color = "inherit",
href = "#/",
`aria-label` = "close",
Icon("close")
),
Typography(sx = list(ml = 2, flex = 1), variant = "h6", component = "div", "Sound"),
Button(autoFocus = TRUE, color = "inherit", href = "#/", "save")
)
),
List(
ListItemButton(ListItemText(primary = "Phone ringtone", secondary = "Titania")),
Divider(),
ListItemButton(
ListItemText(primary = "Default notification ringtone", secondary = "Tethys")
)
)
)
),
Route(path = "*", element = NULL)
)
)
)JS code
import * as React from 'react';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import ListItemText from '@mui/material/ListItemText';
import ListItemButton from '@mui/material/ListItemButton';
import List from '@mui/material/List';
import Divider from '@mui/material/Divider';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import CloseIcon from '@mui/icons-material/Close';
import Slide from '@mui/material/Slide';
const Transition = React.forwardRef(function Transition(props, ref) {
return <Slide direction="up" ref={ref} {...props} />;
});
export default function FullScreenDialog() {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<React.Fragment>
<Button variant="outlined" onClick={handleClickOpen}>
Open full-screen dialog
</Button>
<Dialog
fullScreen
open={open}
onClose={handleClose}
slots={{
transition: Transition,
}}
>
<AppBar sx={{ position: 'relative' }}>
<Toolbar>
<IconButton
edge="start"
color="inherit"
onClick={handleClose}
aria-label="close"
>
<CloseIcon />
</IconButton>
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div">
Sound
</Typography>
<Button autoFocus color="inherit" onClick={handleClose}>
save
</Button>
</Toolbar>
</AppBar>
<List>
<ListItemButton>
<ListItemText primary="Phone ringtone" secondary="Titania" />
</ListItemButton>
<Divider />
<ListItemButton>
<ListItemText
primary="Default notification ringtone"
secondary="Tethys"
/>
</ListItemButton>
</List>
</Dialog>
</React.Fragment>
);
}Optional sizes
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.
Here each combination of maxWidth and
fullWidth is its own route
(#/max-width/<size>/<full|fit>), generated with
lapply(). The Select and the Switch inside the dialog
navigate between those routes.
sizes <- c("false", "xs", "sm", "md", "lg", "xl")
maxWidthRoute <- function(size, fullWidth) {
path <- function(size, fullWidth) {
sprintf("/max-width/%s/%s", size, if (fullWidth) "full" else "fit")
}
Route(
path = path(size, fullWidth),
element = Dialog(
fullWidth = fullWidth,
maxWidth = if (size == "false") FALSE else size,
open = TRUE,
onClose = closeDialog,
DialogTitle("Optional sizes"),
DialogContent(
DialogContentText("You can set my maximum width and whether to adapt or not."),
Box(
noValidate = TRUE,
component = "form",
sx = list(display = "flex", flexDirection = "column", m = "auto", width = "fit-content"),
FormControl(
sx = list(mt = 2, minWidth = 120),
InputLabel(htmlFor = "max-width", "maxWidth"),
Select(
autoFocus = TRUE,
value = size,
label = "maxWidth",
inputProps = list(name = "max-width", id = "max-width"),
onChange = JS(sprintf(
"(event) => { window.location.hash = '/max-width/' + event.target.value + '/%s'; }",
if (fullWidth) "full" else "fit"
)),
lapply(sizes, function(s) MenuItem(value = s, s))
)
),
FormControlLabel(
sx = list(mt = 1),
control = Switch(
checked = fullWidth,
onChange = JS(sprintf(
"() => { window.location.hash = '%s'; }",
path(size, !fullWidth)
))
),
label = "Full width"
)
)
),
DialogActions(Button(href = "#/", "Close"))
)
)
}
muiMaterialPage(
CssBaseline(),
Button(variant = "outlined", href = "#/max-width/sm/full", "Open max-width dialog"),
RouterProvider(
router = createHashRouter(
lapply(sizes, maxWidthRoute, fullWidth = TRUE),
lapply(sizes, maxWidthRoute, fullWidth = FALSE),
Route(path = "*", element = NULL)
)
)
)JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import Switch from '@mui/material/Switch';
export default function MaxWidthDialog() {
const [open, setOpen] = React.useState(false);
const [fullWidth, setFullWidth] = React.useState(true);
const [maxWidth, setMaxWidth] = React.useState('sm');
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const handleMaxWidthChange = (event) => {
setMaxWidth(
// @ts-expect-error autofill of arbitrary value is not handled.
event.target.value,
);
};
const handleFullWidthChange = (event) => {
setFullWidth(event.target.checked);
};
return (
<React.Fragment>
<Button variant="outlined" onClick={handleClickOpen}>
Open max-width dialog
</Button>
<Dialog
fullWidth={fullWidth}
maxWidth={maxWidth}
open={open}
onClose={handleClose}
>
<DialogTitle>Optional sizes</DialogTitle>
<DialogContent>
<DialogContentText>
You can set my maximum width and whether to adapt or not.
</DialogContentText>
<Box
noValidate
component="form"
sx={{
display: 'flex',
flexDirection: 'column',
m: 'auto',
width: 'fit-content',
}}
>
<FormControl sx={{ mt: 2, minWidth: 120 }}>
<InputLabel htmlFor="max-width">maxWidth</InputLabel>
<Select
autoFocus
value={maxWidth}
onChange={handleMaxWidthChange}
label="maxWidth"
inputProps={{
name: 'max-width',
id: 'max-width',
}}
>
<MenuItem value={false}>false</MenuItem>
<MenuItem value="xs">xs</MenuItem>
<MenuItem value="sm">sm</MenuItem>
<MenuItem value="md">md</MenuItem>
<MenuItem value="lg">lg</MenuItem>
<MenuItem value="xl">xl</MenuItem>
</Select>
</FormControl>
<FormControlLabel
sx={{ mt: 1 }}
control={
<Switch checked={fullWidth} onChange={handleFullWidthChange} />
}
label="Full width"
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Close</Button>
</DialogActions>
</Dialog>
</React.Fragment>
);
}Responsive full-screen
In React, you may make a dialog responsively full screen using useMediaQuery.
Hooks are not available from R, but you get the same result with
responsive values in sx: below the md
breakpoint (900px), the dialog paper takes the whole screen.
muiMaterialPage(
CssBaseline(),
Button(variant = "outlined", href = "#/responsive-dialog", "Open responsive dialog"),
RouterProvider(
router = createHashRouter(
Route(
path = "responsive-dialog",
element = Dialog(
open = TRUE,
onClose = closeDialog,
`aria-labelledby` = "responsive-dialog-title",
sx = list(
"& .MuiDialog-paper" = list(
m = list(xs = 0, md = 4),
width = list(xs = "100%", md = "auto"),
maxWidth = list(xs = "100%", md = 600),
height = list(xs = "100%", md = "auto"),
maxHeight = list(xs = "100%", md = "calc(100% - 64px)"),
borderRadius = list(xs = 0, md = 1)
)
),
DialogTitle(id = "responsive-dialog-title", "Use Google's location service?"),
DialogContent(
DialogContentText(
"Let Google help apps determine location. This means sending anonymous
location data to Google, even when no apps are running."
)
),
DialogActions(
Button(autoFocus = TRUE, href = "#/", "Disagree"),
Button(href = "#/", "Agree")
)
)
),
Route(path = "*", element = NULL)
)
)
)JS code
import * as React from 'react';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/material/styles';
export default function ResponsiveDialog() {
const [open, setOpen] = React.useState(false);
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down('md'));
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<React.Fragment>
<Button variant="outlined" onClick={handleClickOpen}>
Open responsive dialog
</Button>
<Dialog
fullScreen={fullScreen}
open={open}
onClose={handleClose}
aria-labelledby="responsive-dialog-title"
>
<DialogTitle id="responsive-dialog-title">
{"Use Google's location service?"}
</DialogTitle>
<DialogContent>
<DialogContentText>
Let Google help apps determine location. This means sending anonymous
location data to Google, even when no apps are running.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleClose}>
Disagree
</Button>
<Button onClick={handleClose} autoFocus>
Agree
</Button>
</DialogActions>
</Dialog>
</React.Fragment>
);
}Confirmation dialogs
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.
The radio group sits in a form. “Ok” submits it and writes the chosen
ringtone to the URL (#/?ringtone=...); “Cancel” goes back
in the browser history, which keeps the previous choice.
ringtones <- c(
"None", "Atria", "Callisto", "Dione", "Ganymede", "Hangouts Call", "Luna",
"Oberon", "Phobos", "Pyxis", "Sedna", "Titania", "Triton", "Umbriel"
)
currentRingtone <- JS("v => v.length ? v[0] : 'Dione'")
goBack <- JS("() => { window.history.back(); }")
muiMaterialPage(
CssBaseline(),
Box(
sx = list(width = "100%", maxWidth = 360, bgcolor = "background.paper"),
RouterProvider(
router = createHashRouter(
Route(
path = "/",
element = List(
component = "div",
role = "group",
ListItemButton(divider = TRUE, disabled = TRUE, ListItemText(primary = "Interruptions")),
ListItemButton(
divider = TRUE,
`aria-haspopup` = "true",
`aria-label` = "phone ringtone",
href = "#/ringtone",
ListItemText(
primary = "Phone ringtone",
secondary = useSearchParams(param = "ringtone", render = currentRingtone)
)
),
ListItemButton(
divider = TRUE,
disabled = TRUE,
ListItemText(primary = "Default notification ringtone", secondary = "Tethys")
),
Outlet()
),
Route(
path = "ringtone",
element = Dialog(
sx = list("& .MuiDialog-paper" = list(width = "80%", maxHeight = 435)),
maxWidth = "xs",
open = TRUE,
onClose = goBack,
DialogTitle("Phone Ringtone"),
DialogContent(
dividers = TRUE,
Box(
component = "form",
id = "ringtone-form",
onSubmit = JS(
"(event) => {
event.preventDefault();
const value = new FormData(event.currentTarget).get('ringtone');
window.location.hash = '/?ringtone=' + encodeURIComponent(value);
}"
),
RadioGroup(
`aria-label` = "ringtone",
name = "ringtone",
defaultValue = "Dione",
lapply(ringtones, function(option) {
FormControlLabel(value = option, control = Radio(), label = option)
})
)
)
),
DialogActions(
Button(autoFocus = TRUE, onClick = goBack, "Cancel"),
Button(type = "submit", form = "ringtone-form", "Ok")
)
)
),
Route(path = "*", element = NULL)
)
)
)
)
)JS code
import * as React from 'react';
import PropTypes from 'prop-types';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import List from '@mui/material/List';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import Dialog from '@mui/material/Dialog';
import RadioGroup from '@mui/material/RadioGroup';
import Radio from '@mui/material/Radio';
import FormControlLabel from '@mui/material/FormControlLabel';
const options = [
'None',
'Atria',
'Callisto',
'Dione',
'Ganymede',
'Hangouts Call',
'Luna',
'Oberon',
'Phobos',
'Pyxis',
'Sedna',
'Titania',
'Triton',
'Umbriel',
];
function ConfirmationDialogRaw(props) {
const { onClose, value: valueProp, open, ...other } = props;
const [value, setValue] = React.useState(valueProp);
const radioGroupRef = React.useRef(null);
React.useEffect(() => {
if (!open) {
setValue(valueProp);
}
}, [valueProp, open]);
const handleEntering = () => {
if (radioGroupRef.current != null) {
radioGroupRef.current.focus();
}
};
const handleCancel = () => {
onClose();
};
const handleOk = () => {
onClose(value);
};
const handleChange = (event) => {
setValue(event.target.value);
};
return (
<Dialog
sx={{ '& .MuiDialog-paper': { width: '80%', maxHeight: 435 } }}
maxWidth="xs"
slotProps={{
transition: {
onEntering: handleEntering,
},
}}
open={open}
{...other}
>
<DialogTitle>Phone Ringtone</DialogTitle>
<DialogContent dividers>
<RadioGroup
ref={radioGroupRef}
aria-label="ringtone"
name="ringtone"
value={value}
onChange={handleChange}
>
{options.map((option) => (
<FormControlLabel
value={option}
key={option}
control={<Radio />}
label={option}
/>
))}
</RadioGroup>
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleOk}>Ok</Button>
</DialogActions>
</Dialog>
);
}
ConfirmationDialogRaw.propTypes = {
onClose: PropTypes.func.isRequired,
open: PropTypes.bool.isRequired,
value: PropTypes.string.isRequired,
};
export default function ConfirmationDialog() {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState('Dione');
const handleClickListItem = () => {
setOpen(true);
};
const handleClose = (newValue) => {
setOpen(false);
if (newValue) {
setValue(newValue);
}
};
return (
<Box sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}>
<List component="div" role="group">
<ListItemButton divider disabled>
<ListItemText primary="Interruptions" />
</ListItemButton>
<ListItemButton
divider
aria-haspopup="true"
aria-controls="ringtone-menu"
aria-label="phone ringtone"
onClick={handleClickListItem}
>
<ListItemText primary="Phone ringtone" secondary={value} />
</ListItemButton>
<ListItemButton divider disabled>
<ListItemText primary="Default notification ringtone" secondary="Tethys" />
</ListItemButton>
<ConfirmationDialogRaw
id="ringtone-menu"
keepMounted
open={open}
onClose={handleClose}
value={value}
/>
</List>
</Box>
);
}Non-modal dialog
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. It is a fixed Paper() with
role = "dialog" and aria-modal = "false": the
page behind it stays usable.
muiMaterialPage(
CssBaseline(),
Button(variant = "outlined", href = "#/cookies-banner", "Show cookie banner"),
RouterProvider(
router = createHashRouter(
Route(
path = "cookies-banner",
element = Paper(
role = "dialog",
`aria-modal` = "false",
`aria-label` = "Cookie banner",
square = TRUE,
variant = "outlined",
tabIndex = -1,
sx = list(
position = "fixed", bottom = 0, left = 0, right = 0, zIndex = "snackbar",
m = 0, p = 2, borderWidth = 0, borderTopWidth = 1
),
Stack(
direction = list(xs = "column", sm = "row"),
sx = list(justifyContent = "space-between", gap = 2),
Box(
sx = list(flexShrink = 1, alignSelf = list(xs = "flex-start", sm = "center")),
Typography(sx = list(fontWeight = "bold"), "This website uses cookies"),
Typography(variant = "body2", "example.com relies on cookies to improve your experience.")
),
Stack(
direction = list(xs = "row-reverse", sm = "row"),
sx = list(gap = 2, flexShrink = 0, alignSelf = list(xs = "flex-end", sm = "center")),
Button(size = "small", href = "#/", variant = "contained", "Allow all"),
Button(size = "small", href = "#/", "Reject all")
)
)
)
),
Route(path = "*", element = NULL)
)
)
)JS code
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>
);
}Draggable dialog
In React, you can create a draggable dialog with react-draggable. That library is not bundled with muiMaterial, so this demo has no R equivalent.
Scrolling long content
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:
scrollDialog <- function(scroll) {
Route(
path = paste0("scroll-dialog-", scroll),
element = Dialog(
open = TRUE,
onClose = closeDialog,
scroll = scroll,
`aria-labelledby` = "scroll-dialog-title",
`aria-describedby` = "scroll-dialog-description",
DialogTitle(id = "scroll-dialog-title", "Subscribe"),
DialogContent(
dividers = scroll == "paper",
DialogContentText(
id = "scroll-dialog-description",
tabIndex = -1,
paste(rep(
"Cras mattis consectetur purus sit amet fermentum.
Cras justo odio, dapibus ac facilisis in, egestas eget quam.
Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Praesent commodo cursus magna, vel scelerisque nisl consectetur et.",
50
), collapse = " ")
)
),
DialogActions(
Button(href = "#/", "Cancel"),
Button(href = "#/", "Subscribe")
)
)
)
}
muiMaterialPage(
CssBaseline(),
Button(href = "#/scroll-dialog-paper", "scroll=paper"),
Button(href = "#/scroll-dialog-body", "scroll=body"),
RouterProvider(
router = createHashRouter(
scrollDialog("paper"),
scrollDialog("body"),
Route(path = "*", element = NULL)
)
)
)JS code
import * as React from 'react';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
export default function ScrollDialog() {
const [open, setOpen] = React.useState(false);
const [scroll, setScroll] = React.useState('paper');
const handleClickOpen = (scrollType) => () => {
setOpen(true);
setScroll(scrollType);
};
const handleClose = () => {
setOpen(false);
};
const descriptionElementRef = React.useRef(null);
React.useEffect(() => {
if (open) {
const { current: descriptionElement } = descriptionElementRef;
if (descriptionElement !== null) {
descriptionElement.focus();
}
}
}, [open]);
return (
<React.Fragment>
<Button onClick={handleClickOpen('paper')}>scroll=paper</Button>
<Button onClick={handleClickOpen('body')}>scroll=body</Button>
<Dialog
open={open}
onClose={handleClose}
scroll={scroll}
aria-labelledby="scroll-dialog-title"
aria-describedby="scroll-dialog-description"
>
<DialogTitle id="scroll-dialog-title">Subscribe</DialogTitle>
<DialogContent dividers={scroll === 'paper'}>
<DialogContentText
id="scroll-dialog-description"
ref={descriptionElementRef}
tabIndex={-1}
>
{[...new Array(50)]
.map(
() => `Cras mattis consectetur purus sit amet fermentum.
Cras justo odio, dapibus ac facilisis in, egestas eget quam.
Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Praesent commodo cursus magna, vel scelerisque nisl consectetur et.`,
)
.join('\n')}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleClose}>Subscribe</Button>
</DialogActions>
</Dialog>
</React.Fragment>
);
}Performance
Follow the Modal performance section.
Limitations
Follow the Modal limitations section.
Accessibility
Follow the Modal accessibility section.
Dialogs in Shiny apps
The router pattern above works unchanged inside a Shiny app. muiMaterial also has two dedicated wrappers:
-
Dialog.triggerId()binds the dialog to any element by itsid. The open/close state is handled in the browser. It closes on a backdrop click or Escape, and a caller-suppliedonCloseruns afterwards. See Overlays with.triggerId. -
Dialog.shinyInput()lets the server decide when the dialog opens. For example, open it after a validation step, or keep it open until the server has saved data. The wrapper does not manageopen: render it withopen = FALSEand toggle it withupdateDialog.shinyInput().
library(shiny)
library(muiMaterial)
ui <- muiMaterialPage(
CssBaseline(),
Box(
sx = list(p = 2),
Button.shinyInput("delete", "Delete file", variant = "contained", color = "error"),
Dialog.shinyInput(
inputId = "dialog",
open = FALSE,
# Backdrop click / Escape send an event to the server
onClose = triggerEvent("dialog_close"),
DialogTitle("Delete this file?"),
DialogContent(DialogContentText("This action cannot be undone.")),
DialogActions(
Button.shinyInput("cancel", "Cancel"),
Button.shinyInput("confirm", "Delete", color = "error", variant = "contained")
)
),
verbatimTextOutput("result")
)
)
server <- function(input, output, session) {
result <- reactiveVal("Nothing deleted yet")
closeDialog <- function() updateDialog.shinyInput(inputId = "dialog", open = FALSE)
observeEvent(input$delete, updateDialog.shinyInput(inputId = "dialog", open = TRUE))
observeEvent(input$dialog_close, closeDialog())
observeEvent(input$cancel, closeDialog())
observeEvent(input$confirm, {
result("File deleted")
closeDialog()
})
output$result <- renderText(result())
}
shinyApp(ui, server)input$dialog itself is a click counter: it increments on
every click inside the dialog surface. Most apps ignore it and listen to
the action buttons instead. Run the bundled version with
muiMaterialExample("Dialog").
