Reference
Modal
The modal component offers a robust foundation for building dialogs, popovers, lightboxes, and other similar elements.
The component displays its
children
node in front of a backdrop. The Modal
provides key features:- 💄 Manages modal stacking when one-at-a-time just isn't enough.
- 🔐 Creates a backdrop, for disabling interaction below the modal.
- 🔐 It disables scrolling of the page content while open.
- ♿️ It properly manages focus; moving to the modal content, and keeping it there until the modal is closed.
- ♿️ Adds the appropriate ARIA roles automatically.
The term "modal" is often used interchangeably with "dialog," but this is inaccurate. A modal window refers to a UI element that blocks interaction with the rest of the application.
For creating modal dialogs, consider using the Dialog component instead of directly using Modal. Modal is a lower-level construct utilized by the following components:
Basic modal
import * as React from 'react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Typography from '@mui/material/Typography'; import Modal from '@mui/material/Modal'; const style = { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: 400, bgcolor: 'background.paper', border: '2px solid #000', boxShadow: 24, p: 4, }; export default function BasicModal() { const [open, setOpen] = React.useState(false); const handleOpen = () => setOpen(true); const handleClose = () => setOpen(false); return ( <div> <Button onClick={handleOpen}>Open modal</Button> <Modal open={open} onClose={handleClose} aria-labelledby="modal-modal-title" aria-describedby="modal-modal-description" > <Box sx={style}> <Typography id="modal-modal-title" variant="h6" component="h2"> Text in a modal </Typography> <Typography id="modal-modal-description" sx={{ mt: 2 }}> Duis mollis, est non commodo luctus, nisi erat porttitor ligula. </Typography> </Box> </Modal> </div> ); }
Note that you can remove the outline (often blue or gold) by applying the
outline: 0
CSS property.Nested modal
Modals can be nested, such as including a select within a dialog, but stacking more than two modals or using multiple modals with backdrops is not recommended.
import * as React from 'react'; import Box from '@mui/material/Box'; import Modal from '@mui/material/Modal'; import Button from '@mui/material/Button'; const style = { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: 400, bgcolor: 'background.paper', border: '2px solid #000', boxShadow: 24, pt: 2, px: 4, pb: 3, }; function ChildModal() { const [open, setOpen] = React.useState(false); const handleOpen = () => { setOpen(true); }; const handleClose = () => { setOpen(false); }; return ( <React.Fragment> <Button onClick={handleOpen}>Open Child Modal</Button> <Modal open={open} onClose={handleClose} aria-labelledby="child-modal-title" aria-describedby="child-modal-description" > <Box sx={{ ...style, width: 200 }}> <h2 id="child-modal-title">Text in a child modal</h2> <p id="child-modal-description"> Lorem ipsum, dolor sit amet consectetur adipisicing elit. </p> <Button onClick={handleClose}>Close Child Modal</Button> </Box> </Modal> </React.Fragment> ); } export default function NestedModal() { const [open, setOpen] = React.useState(false); const handleOpen = () => { setOpen(true); }; const handleClose = () => { setOpen(false); }; return ( <div> <Button onClick={handleOpen}>Open modal</Button> <Modal open={open} onClose={handleClose} aria-labelledby="parent-modal-title" aria-describedby="parent-modal-description" > <Box sx={{ ...style, width: 400 }}> <h2 id="parent-modal-title">Text in a modal</h2> <p id="parent-modal-description"> Duis mollis, est non commodo luctus, nisi erat porttitor ligula. </p> <ChildModal /> </Box> </Modal> </div> ); }
Transitions
The modal's open/close state can be animated using a transition component. This component must adhere to the following requirements:
- Be a direct child descendent of the modal.
- Have an
in
prop. This corresponds to the open/close state. - Call the
onEnter
callback prop when the enter transition starts. - Call the
onExited
callback prop when the exit transition is completed. These two callbacks allow the modal to unmount the child content when closed and fully transitioned.
Modal includes built-in support for react-transition-group.
import * as React from 'react'; import Backdrop from '@mui/material/Backdrop'; import Box from '@mui/material/Box'; import Modal from '@mui/material/Modal'; import Fade from '@mui/material/Fade'; import Button from '@mui/material/Button'; import Typography from '@mui/material/Typography'; const style = { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: 400, bgcolor: 'background.paper', border: '2px solid #000', boxShadow: 24, p: 4, }; export default function TransitionsModal() { const [open, setOpen] = React.useState(false); const handleOpen = () => setOpen(true); const handleClose = () => setOpen(false); return ( <div> <Button onClick={handleOpen}>Open modal</Button> <Modal aria-labelledby="transition-modal-title" aria-describedby="transition-modal-description" open={open} onClose={handleClose} closeAfterTransition slots={{ backdrop: Backdrop }} slotProps={{ backdrop: { timeout: 500, }, }} > <Fade in={open}> <Box sx={style}> <Typography id="transition-modal-title" variant="h6" component="h2"> Text in a modal </Typography> <Typography id="transition-modal-description" sx={{ mt: 2 }}> Duis mollis, est non commodo luctus, nisi erat porttitor ligula. </Typography> </Box> </Fade> </Modal> </div> ); }
Alternatively, you can utilize react spring for transitions.
import * as React from 'react'; import Backdrop from '@mui/material/Backdrop'; import Box from '@mui/material/Box'; import Modal from '@mui/material/Modal'; import Button from '@mui/material/Button'; import Typography from '@mui/material/Typography'; import { useSpring, animated } from '@react-spring/web'; interface FadeProps { children: React.ReactElement<any>; in?: boolean; onClick?: any; onEnter?: (node: HTMLElement, isAppearing: boolean) => void; onExited?: (node: HTMLElement, isAppearing: boolean) => void; ownerState?: any; } const Fade = React.forwardRef<HTMLDivElement, FadeProps>(function Fade(props, ref) { const { children, in: open, onClick, onEnter, onExited, ownerState, ...other } = props; const style = useSpring({ from: { opacity: 0 }, to: { opacity: open ? 1 : 0 }, onStart: () => { if (open && onEnter) { onEnter(null as any, true); } }, onRest: () => { if (!open && onExited) { onExited(null as any, true); } }, }); return ( // @ts-expect-error https://github.com/pmndrs/react-spring/issues/2341 <animated.div ref={ref} style={style} {...other}> {React.cloneElement(children, { onClick })} </animated.div> ); }); const style = { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: 400, bgcolor: 'background.paper', border: '2px solid #000', boxShadow: 24, p: 4, }; export default function SpringModal() { const [open, setOpen] = React.useState(false); const handleOpen = () => setOpen(true); const handleClose = () => setOpen(false); return ( <div> <Button onClick={handleOpen}>Open modal</Button> <Modal aria-labelledby="spring-modal-title" aria-describedby="spring-modal-description" open={open} onClose={handleClose} closeAfterTransition slots={{ backdrop: Backdrop }} slotProps={{ backdrop: { TransitionComponent: Fade, }, }} > <Fade in={open}> <Box sx={style}> <Typography id="spring-modal-title" variant="h6" component="h2"> Text in a modal </Typography> <Typography id="spring-modal-description" sx={{ mt: 2 }}> Duis mollis, est non commodo luctus, nisi erat porttitor ligula. </Typography> </Box> </Fade> </Modal> </div> ); }
Performance
By default, modal content is unmounted when closed. To make content accessible to search engines or to render complex component trees while maintaining interaction responsiveness, consider enabling the
keepMounted
prop:<Modal keepMounted />
import * as React from 'react'; import Box from '@mui/material/Box'; import Modal from '@mui/material/Modal'; import Button from '@mui/material/Button'; import Typography from '@mui/material/Typography'; const style = { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: 400, bgcolor: 'background.paper', border: '2px solid #000', boxShadow: 24, p: 4, }; export default function KeepMountedModal() { const [open, setOpen] = React.useState(false); const handleOpen = () => setOpen(true); const handleClose = () => setOpen(false); return ( <div> <Button onClick={handleOpen}>Open modal</Button> <Modal keepMounted open={open} onClose={handleClose} aria-labelledby="keep-mounted-modal-title" aria-describedby="keep-mounted-modal-description" > <Box sx={style}> <Typography id="keep-mounted-modal-title" variant="h6" component="h2"> Text in a modal </Typography> <Typography id="keep-mounted-modal-description" sx={{ mt: 2 }}> Duis mollis, est non commodo luctus, nisi erat porttitor ligula. </Typography> </Box> </Modal> </div> ); }
As with any performance optimization, this approach is not a universal solution. Identify performance bottlenecks before applying these optimization techniques.
Server-side modal
React does not support the
createPortal()
API on the server. To display a modal on the server, disable the portal feature using the disablePortal
prop:Server-side modal
If you disable JavaScript, you will still see me.
import * as React from 'react'; import Modal from '@mui/material/Modal'; import Typography from '@mui/material/Typography'; import Box from '@mui/material/Box'; export default function ServerModal() { const rootRef = React.useRef<HTMLDivElement>(null); return ( <Box sx={{ height: 300, flexGrow: 1, minWidth: 300, transform: 'translateZ(0)', }} ref={rootRef} > <Modal disablePortal disableEnforceFocus disableAutoFocus open aria-labelledby="server-modal-title" aria-describedby="server-modal-description" sx={{ display: 'flex', p: 1, alignItems: 'center', justifyContent: 'center', }} container={() => rootRef.current!} > <Box sx={(theme) => ({ position: 'relative', width: 400, bgcolor: 'background.paper', border: '2px solid #000', boxShadow: theme.shadows[5], p: 4, })} > <Typography id="server-modal-title" variant="h6" component="h2"> Server-side modal </Typography> <Typography id="server-modal-description" sx={{ pt: 2 }}> If you disable JavaScript, you will still see me. </Typography> </Box> </Modal> </Box> ); }
Limitations
Focus trap
The modal ensures focus remains within the component by redirecting it to the modal body if it attempts to escape.
This behavior enhances accessibility but may cause issues. If users need to interact with other page elements, such as a chatbot window, you can disable this behavior:
<Modal disableEnforceFocus />
Accessibility
- Ensure you include
aria-labelledby="id..."
, referencing the modal title, in theModal
. You may also provide a description using thearia-describedby="id..."
prop on theModal
.<Modal aria-labelledby="modal-title" aria-describedby="modal-description"> <h2 id="modal-title">My Title</h2> <Typography id="modal-description">My Description</Typography> </Modal>
- The WAI-ARIA Authoring Practices can guide you in setting the initial focus on the most relevant element within your modal content.
- Remember that a "modal window" overlays either the primary window or another modal window. Content beneath a modal is inert, meaning users cannot interact with it. This may lead to conflicting behaviors.