Building Centered Modal Popups with React MUI: A Complete Guide
Modal dialogs are a crucial UI component for any modern web application. They allow you to display content that temporarily blocks interactions with the main view, focusing user attention on important information or actions. Material UI (MUI), one of the most popular React component libraries, provides a robust Modal component that simplifies creating accessible, responsive overlay popups.
In this comprehensive guide, I'll walk you through everything you need to know about using MUI's Modal component to create perfectly centered, responsive, and accessible popup overlays. We'll start with the basics and progress to advanced customization techniques that I've refined over years of front-end development.
What You'll Learn
By the end of this article, you'll be able to:
- Implement basic and advanced MUI Modal components
- Center content perfectly both horizontally and vertically
- Style modals using MUI's styling approaches
- Create reusable modal components for your applications
- Handle modal state and transitions effectively
- Implement accessibility features for inclusive user experiences
- Troubleshoot common modal implementation challenges
Understanding MUI's Modal Component
The Modal component in Material UI serves as a foundation for creating dialogs, popovers, lightboxes, and other overlay elements. It's important to understand that Modal is a lower-level construct that provides core functionality, while Dialog (which uses Modal internally) offers a more opinionated, ready-to-use implementation.
Core Functionality and Architecture
At its heart, the Modal component provides several key features:
- Overlay Management: Creates a backdrop that blocks interaction with the underlying page
- Focus Trapping: Keeps keyboard focus within the modal when open
- Keyboard Navigation: Handles ESC key presses to close the modal
- Accessibility: Manages proper ARIA attributes for screen readers
- Portal Integration: Renders content at the end of the document body by default
The Modal doesn't impose styling on its children - it only manages the overlay and accessibility aspects. This gives you complete freedom to design the modal's contents while ensuring proper behavior.
Essential Props Reference
The Modal component accepts numerous props that control its behavior and appearance. Here are the most important ones:
Prop | Type | Default | Description |
---|---|---|---|
open | boolean | required | Controls whether the modal is displayed |
children | node | required | The content to be displayed in the modal |
onClose | function | - | Callback fired when the modal should close |
BackdropComponent | element type | Backdrop | Component used for the backdrop |
BackdropProps | object | Props applied to the Backdrop element | |
closeAfterTransition | boolean | false | Wait for transition to finish before removing from DOM |
component | element type | 'div' | The component used for the root node |
components | object | Customizes the component parts used | |
componentsProps | object | Props for custom components | |
container | HTML element or function | - | An HTML element or function that returns one to use as portal container |
disableAutoFocus | boolean | false | Disables automatic focus on first focusable element |
disableEnforceFocus | boolean | false | Disables focus containment within modal |
disableEscapeKeyDown | boolean | false | Disables ESC key closing the modal |
disablePortal | boolean | false | Disables the portal behavior |
disableRestoreFocus | boolean | false | Disables restoring focus to previous element after modal closes |
disableScrollLock | boolean | false | Disables scrolling of the page content while modal is open |
hideBackdrop | boolean | false | Hides the backdrop element |
keepMounted | boolean | false | Always keeps the children in the DOM |
sx | object, array, function | - | The system prop for defining system overrides and custom styles |
Understanding Modal vs. Dialog
Before we dive deeper, it's important to understand when to use Modal versus Dialog:
- Use Modal when: You need complete control over the styling and behavior of your popup, or when building a custom UI component that requires overlay functionality.
- Use Dialog when: You want a pre-styled, opinionated dialog box with title, content, and action areas already configured according to Material Design guidelines.
In this guide, we'll focus on Modal since it gives us more flexibility and helps understand the underlying mechanics better.
Basic Implementation: Creating Your First Centered Modal
Let's start with a simple implementation of a centered modal popup. The key challenge with modals is proper centering, which we'll solve using MUI's styling system.
Step 1: Set Up Your Project
First, make sure you have the necessary dependencies installed:
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled
Step 2: Create a Basic Modal Component
Let's create a simple modal that opens and closes with a button:
import React, { useState } from 'react';
import { Box, Button, Modal, Typography } from '@mui/material';
// Style for the modal content box
const style = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
boxShadow: 24,
borderRadius: 2,
p: 4,
};
function BasicModal() {
const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
return (
<div>
<Button onClick={handleOpen} variant="contained">Open Modal</Button>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<Box sx={style}>
<Typography id="modal-title" variant="h6" component="h2">
Hello, I'm a centered modal!
</Typography>
<Typography id="modal-description" sx={{ mt: 2 }}>
This modal is perfectly centered both horizontally and vertically.
Click anywhere outside to close it.
</Typography>
</Box>
</Modal>
</div>
);
}
export default BasicModal;
In this example, I've created a basic modal with the following key elements:
- A state variable
open
to control the modal's visibility - Handler functions for opening and closing the modal
- A Button component to trigger the modal
- The Modal component with required props:
open
: Controls visibility based on stateonClose
: Function to call when the modal should close- Accessibility attributes for screen readers
- A styled Box component that serves as the modal content container
The most important part for centering is the style
object. The combination of:
position: 'absolute'
top: '50%'
andleft: '50%'
transform: 'translate(-50%, -50%)'
This is a reliable CSS technique for perfect centering that works across browsers and device sizes.
Step 3: Understanding the Modal Backdrop
The modal backdrop is the semi-transparent overlay that appears behind the modal content. It helps focus attention on the modal by dimming the rest of the page and provides a clickable area to dismiss the modal.
By default, MUI's Modal includes a backdrop, but you can customize it:
import React, { useState } from 'react';
import { Box, Button, Modal, Typography, Backdrop } from '@mui/material';
function CustomBackdropModal() {
const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
return (
<div>
<Button onClick={handleOpen} variant="contained">Open Modal with Custom Backdrop</Button>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="backdrop-modal-title"
aria-describedby="backdrop-modal-description"
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
sx: { backgroundColor: 'rgba(0, 0, 0, 0.8)' } // Darker backdrop
}}
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
boxShadow: 24,
borderRadius: 2,
p: 4,
}}
>
<Typography id="backdrop-modal-title" variant="h6" component="h2">
Modal with Custom Backdrop
</Typography>
<Typography id="backdrop-modal-description" sx={{ mt: 2 }}>
Notice the darker backdrop behind this modal.
</Typography>
</Box>
</Modal>
</div>
);
}
export default CustomBackdropModal;
In this example, I've customized the backdrop by:
- Explicitly using the Backdrop component
- Setting a custom timeout for animations
- Using a darker background color with the
sx
prop
This approach gives you control over the appearance and behavior of the backdrop.
Adding Transitions and Animations
Static modals can feel jarring when they suddenly appear. Adding transitions makes the user experience smoother and more polished.
Step 4: Implementing Fade Transitions
MUI provides a Fade component that we can use to animate our modal's entrance and exit:
import React, { useState } from 'react';
import { Box, Button, Modal, Typography, Fade, Backdrop } from '@mui/material';
function TransitionModal() {
const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
return (
<div>
<Button onClick={handleOpen} variant="contained">Open Modal with Transitions</Button>
<Modal
open={open}
onClose={handleClose}
closeAfterTransition
slots={{ backdrop: Backdrop }}
slotProps={{
backdrop: {
timeout: 500,
},
}}
>
<Fade in={open}>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
boxShadow: 24,
borderRadius: 2,
p: 4,
}}
>
<Typography variant="h6" component="h2">
Smooth Transition Modal
</Typography>
<Typography sx={{ mt: 2 }}>
This modal fades in and out smoothly.
</Typography>
</Box>
</Fade>
</Modal>
</div>
);
}
export default TransitionModal;
In this example:
- I've wrapped the modal content in a
Fade
component - Set
closeAfterTransition
to ensure the modal waits for the exit animation to complete - Used the newer
slots
andslotProps
API (which replaces the olderBackdropComponent
andBackdropProps
) - Set the backdrop timeout to match the fade transition
The result is a modal that smoothly fades in and out, creating a more polished user experience.
Step 5: Creating Custom Transitions
For more distinctive entrances, you can create custom transitions using MUI's transition components:
import React, { useState } from 'react';
import { Box, Button, Modal, Typography, Zoom, Backdrop } from '@mui/material';
function ZoomModal() {
const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
return (
<div>
<Button onClick={handleOpen} variant="contained">Open Zoom Modal</Button>
<Modal
open={open}
onClose={handleClose}
closeAfterTransition
slots={{ backdrop: Backdrop }}
slotProps={{
backdrop: {
timeout: 500,
},
}}
>
<Zoom in={open}>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
boxShadow: 24,
borderRadius: 2,
p: 4,
}}
>
<Typography variant="h6" component="h2">
Zoom Transition Modal
</Typography>
<Typography sx={{ mt: 2 }}>
This modal zooms in and out instead of fading.
</Typography>
</Box>
</Zoom>
</Modal>
</div>
);
}
export default ZoomModal;
MUI provides several transition components you can use:
Fade
: Simple opacity transitionGrow
: Combines scale and fadeSlide
: Slides in from the edgeZoom
: Scale transition from the centerCollapse
: Vertical collapse transition
You can choose the one that best fits your design aesthetic.
Creating a Reusable Modal Component
Now that we understand the basics, let's create a reusable modal component that can be used throughout an application.
Step 6: Building a Flexible Modal Component
import React from 'react';
import { Box, Modal, Typography, IconButton, Fade, Backdrop } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
const modalStyle = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: { xs: '90%', sm: 500 },
bgcolor: 'background.paper',
boxShadow: 24,
borderRadius: 2,
p: { xs: 2, sm: 4 },
maxHeight: '90vh',
overflow: 'auto',
};
const headerStyle = {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 2,
};
function ReusableModal({
open,
onClose,
title,
children,
showCloseButton = true,
maxWidth,
fullWidth = false,
...props
}) {
// Calculate the final width based on props
const finalWidth = maxWidth
? { xs: fullWidth ? '90%' : 'auto', sm: maxWidth }
: modalStyle.width;
return (
<Modal
open={open}
onClose={onClose}
closeAfterTransition
slots={{ backdrop: Backdrop }}
slotProps={{
backdrop: {
timeout: 500,
},
}}
{...props}
>
<Fade in={open}>
<Box sx={{ ...modalStyle, width: finalWidth }}>
{title && (
<Box sx={headerStyle}>
<Typography variant="h6" component="h2">
{title}
</Typography>
{showCloseButton && (
<IconButton
aria-label="close"
onClick={onClose}
size="small"
>
<CloseIcon />
</IconButton>
)}
</Box>
)}
{children}
</Box>
</Fade>
</Modal>
);
}
export default ReusableModal;
This reusable component provides:
- Responsive width that adapts to screen size
- Optional title with automatic styling
- Optional close button in the header
- Fade transition built-in
- Customizable max width and full width options
- Scrollable content for modals with lots of content
- Passes through any additional props to the underlying Modal component
Step 7: Using the Reusable Modal
Now let's see how to use our reusable modal component:
import React, { useState } from 'react';
import { Button, Typography, TextField, Stack } from '@mui/material';
import ReusableModal from './ReusableModal';
function ModalDemo() {
const [basicModalOpen, setBasicModalOpen] = useState(false);
const [formModalOpen, setFormModalOpen] = useState(false);
const [wideModalOpen, setWideModalOpen] = useState(false);
return (
<Stack spacing={2} direction="row" sx={{ mb: 4 }}>
{/* Basic Modal Example */}
<Button variant="contained" onClick={() => setBasicModalOpen(true)}>
Basic Modal
</Button>
<ReusableModal
open={basicModalOpen}
onClose={() => setBasicModalOpen(false)}
title="Basic Modal Example"
>
<Typography>
This is a simple modal with just some text content.
It demonstrates the basic usage of our reusable modal component.
</Typography>
</ReusableModal>
{/* Form Modal Example */}
<Button variant="contained" onClick={() => setFormModalOpen(true)}>
Form Modal
</Button>
<ReusableModal
open={formModalOpen}
onClose={() => setFormModalOpen(false)}
title="Contact Form"
aria-labelledby="form-modal-title"
aria-describedby="form-modal-description"
>
<Typography id="form-modal-description" sx={{ mb: 2 }}>
Please fill out the form below to contact us.
</Typography>
<form>
<Stack spacing={2}>
<TextField label="Name" fullWidth />
<TextField label="Email" fullWidth type="email" />
<TextField label="Message" fullWidth multiline rows={4} />
<Button variant="contained" type="submit">
Submit
</Button>
</Stack>
</form>
</ReusableModal>
{/* Wide Modal Example */}
<Button variant="contained" onClick={() => setWideModalOpen(true)}>
Wide Modal
</Button>
<ReusableModal
open={wideModalOpen}
onClose={() => setWideModalOpen(false)}
title="Wide Modal Example"
maxWidth={800}
fullWidth
>
<Typography>
This modal uses the maxWidth and fullWidth props to create a wider modal.
It's useful for displaying tables, large forms, or detailed information.
</Typography>
</ReusableModal>
</Stack>
);
}
export default ModalDemo;
This example demonstrates three different ways to use our reusable modal:
- A basic modal with just text content
- A form modal with interactive elements
- A wide modal that takes advantage of the width customization options
The reusable component makes it easy to maintain consistent styling and behavior across all modals in your application.
Advanced Modal Customization
Now let's explore more advanced customization options for your modals.
Step 8: Styling with Theme Overrides
You can customize the default appearance of all modals in your application by overriding the theme:
import React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import App from './App';
const theme = createTheme({
components: {
// Override the Modal component
MuiModal: {
styleOverrides: {
root: {
// Apply styles to all modals
},
backdrop: {
// Custom backdrop styles
backgroundColor: 'rgba(0, 0, 0, 0.7)',
},
},
},
// You can also override the Backdrop component directly
MuiBackdrop: {
styleOverrides: {
root: {
backdropFilter: 'blur(3px)', // Add blur effect to backdrops
},
},
},
},
});
function ThemedApp() {
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<App />
</ThemeProvider>
);
}
export default ThemedApp;
This approach allows you to set global styles for all modals in your application, ensuring consistency without having to repeat style definitions.
Step 9: Creating a Fullscreen Modal
Sometimes you need a modal that takes up the entire screen, especially on mobile devices:
import React, { useState } from 'react';
import {
Box,
Button,
Modal,
AppBar,
Toolbar,
IconButton,
Typography,
Container,
useTheme,
useMediaQuery
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
function FullscreenModal() {
const [open, setOpen] = useState(false);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
return (
<div>
<Button variant="contained" onClick={handleOpen}>
Open Fullscreen Modal
</Button>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="fullscreen-modal-title"
>
<Box sx={{
height: '100%',
width: '100%',
bgcolor: 'background.paper',
overflow: 'auto'
}}>
<AppBar position="sticky">
<Toolbar>
<Typography variant="h6" component="h2" sx={{ flexGrow: 1 }} id="fullscreen-modal-title">
Fullscreen Modal
</Typography>
<IconButton
edge="end"
color="inherit"
onClick={handleClose}
aria-label="close"
>
<CloseIcon />
</IconButton>
</Toolbar>
</AppBar>
<Container sx={{ py: 4 }}>
<Typography paragraph>
This modal takes up the entire screen, which is useful for complex interfaces
or when you need to show a lot of content.
</Typography>
<Typography paragraph>
On mobile devices, fullscreen modals are often more user-friendly than
centered modals because they provide more space for content and are easier
to interact with.
</Typography>
<Typography paragraph>
The AppBar at the top provides a consistent way to close the modal and
shows the user what they're looking at.
</Typography>
{/* Add more content as needed */}
{Array.from(new Array(10)).map((_, index) => (
<Typography key={index} paragraph>
This is paragraph {index + 1} of demo content to show scrolling.
</Typography>
))}
</Container>
</Box>
</Modal>
</div>
);
}
export default FullscreenModal;
This fullscreen modal:
- Takes up the entire viewport
- Has an AppBar with a title and close button
- Contains scrollable content in a Container
- Works well on both mobile and desktop
It's particularly useful for complex interfaces or when you need to display a lot of information.
Step 10: Creating a Responsive Modal
Let's create a modal that adapts its layout based on screen size:
import React, { useState } from 'react';
import {
Box,
Button,
Modal,
Typography,
useTheme,
useMediaQuery,
Grid,
Divider,
IconButton
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
function ResponsiveModal() {
const [open, setOpen] = useState(false);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
return (
<div>
<Button variant="contained" onClick={handleOpen}>
Open Responsive Modal
</Button>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="responsive-modal-title"
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: isMobile ? '95%' : '80%',
maxWidth: 900,
maxHeight: '90vh',
bgcolor: 'background.paper',
boxShadow: 24,
borderRadius: 2,
overflow: 'auto',
p: 0, // No padding on the container
}}
>
<Box sx={{
p: 2,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
borderBottom: 1,
borderColor: 'divider'
}}>
<Typography variant="h6" component="h2" id="responsive-modal-title">
Product Details
</Typography>
<IconButton onClick={handleClose} aria-label="close">
<CloseIcon />
</IconButton>
</Box>
<Grid container>
{/* Image section - takes full width on mobile, half on desktop */}
<Grid item xs={12} md={6} sx={{
height: isMobile ? '200px' : '400px',
backgroundImage: 'url(https://source.unsplash.com/random/800x800/?product)',
backgroundSize: 'cover',
backgroundPosition: 'center',
}} />
{/* Content section */}
<Grid item xs={12} md={6} sx={{ p: 3 }}>
<Typography variant="h5" gutterBottom>
Premium Product
</Typography>
<Typography variant="subtitle1" color="text.secondary" gutterBottom>
$99.99
</Typography>
<Divider sx={{ my: 2 }} />
<Typography paragraph>
This responsive modal changes its layout based on screen size.
On mobile devices, the image appears above the content.
On desktop, they appear side by side.
</Typography>
<Typography paragraph>
This pattern is useful for product details, user profiles,
or any content that benefits from a different layout on different devices.
</Typography>
<Box sx={{ mt: 3 }}>
<Button variant="contained" fullWidth={isMobile}>
Add to Cart
</Button>
</Box>
</Grid>
</Grid>
</Box>
</Modal>
</div>
);
}
export default ResponsiveModal;
This responsive modal:
- Uses
useMediaQuery
to detect screen size - Changes layout based on screen size (stacked on mobile, side-by-side on desktop)
- Adjusts image height, button width, and overall modal width
- Maintains consistent header and scrolling behavior
Responsive modals provide a better user experience across devices without requiring separate implementations.
Accessibility and Best Practices
Accessibility is crucial for modals since they can create barriers for users with disabilities if not implemented correctly.
Step 11: Implementing Accessible Modals
Let's create a fully accessible modal:
import React, { useState, useRef, useEffect } from 'react';
import {
Box,
Button,
Modal,
Typography,
IconButton,
FocusTrap
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
function AccessibleModal() {
const [open, setOpen] = useState(false);
const modalRef = useRef(null);
const previousFocusRef = useRef(null);
const handleOpen = () => {
// Store the element that had focus before opening the modal
previousFocusRef.current = document.activeElement;
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
// Return focus to the previous element when the modal closes
useEffect(() => {
if (!open && previousFocusRef.current) {
previousFocusRef.current.focus();
}
}, [open]);
// Handle ESC key press
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
handleClose();
}
};
return (
<div>
<Button
variant="contained"
onClick={handleOpen}
aria-haspopup="dialog"
>
Open Accessible Modal
</Button>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="accessible-modal-title"
aria-describedby="accessible-modal-description"
// The following props enhance accessibility
disableAutoFocus={false}
disableEnforceFocus={false}
disableRestoreFocus={false}
onKeyDown={handleKeyDown}
>
<Box
ref={modalRef}
role="dialog"
aria-modal="true"
tabIndex={-1}
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
boxShadow: 24,
borderRadius: 2,
p: 4,
}}
>
<Box sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 2
}}>
<Typography
id="accessible-modal-title"
variant="h6"
component="h2"
>
Accessible Modal
</Typography>
<IconButton
onClick={handleClose}
aria-label="Close modal"
edge="end"
size="small"
>
<CloseIcon />
</IconButton>
</Box>
<Typography id="accessible-modal-description" sx={{ mb: 2 }}>
This modal follows accessibility best practices:
</Typography>
<ul>
<li>Proper ARIA attributes</li>
<li>Focus management</li>
<li>Keyboard navigation support</li>
<li>Screen reader announcements</li>
</ul>
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}>
<Button
variant="contained"
onClick={handleClose}
autoFocus
>
Close
</Button>
</Box>
</Box>
</Modal>
</div>
);
}
export default AccessibleModal;
This accessible modal implements several best practices:
-
Proper ARIA attributes:
aria-labelledby
andaria-describedby
to provide context to screen readersaria-modal="true"
to indicate it's a modal dialogrole="dialog"
to specify the role
-
Focus management:
- Stores and restores focus when the modal opens and closes
- Sets
autoFocus
on the primary action button - Uses MUI's built-in focus trap
-
Keyboard navigation:
- Handles ESC key press to close the modal
- Ensures all interactive elements are focusable
-
Visual design:
- Clear visual hierarchy with a distinct header
- Close button with an accessible label
- Sufficient color contrast
These practices ensure that all users, including those with disabilities, can effectively interact with your modals.
Advanced Use Cases
Let's explore some advanced use cases for modals that solve common UI challenges.
Step 12: Creating a Confirmation Dialog
Confirmation dialogs are a common use case for modals:
import React, { useState } from 'react';
import {
Box,
Button,
Modal,
Typography,
Stack
} from '@mui/material';
function ConfirmationModal({
open,
onClose,
onConfirm,
title = "Confirm Action",
message = "Are you sure you want to proceed?",
confirmText = "Confirm",
cancelText = "Cancel",
severity = "warning" // 'warning', 'error', 'info', 'success'
}) {
// Map severity to color
const colorMap = {
warning: 'warning.main',
error: 'error.main',
info: 'info.main',
success: 'success.main'
};
const color = colorMap[severity] || colorMap.warning;
return (
<Modal
open={open}
onClose={onClose}
aria-labelledby="confirmation-modal-title"
aria-describedby="confirmation-modal-description"
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
boxShadow: 24,
borderRadius: 2,
p: 4,
borderTop: 4,
borderColor: color,
}}
>
<Typography id="confirmation-modal-title" variant="h6" component="h2" gutterBottom>
{title}
</Typography>
<Typography id="confirmation-modal-description" sx={{ mb: 3 }}>
{message}
</Typography>
<Stack direction="row" spacing={2} justifyContent="flex-end">
<Button variant="outlined" onClick={onClose}>
{cancelText}
</Button>
<Button
variant="contained"
onClick={() => {
onConfirm();
onClose();
}}
color={severity === 'error' ? 'error' : 'primary'}
autoFocus
>
{confirmText}
</Button>
</Stack>
</Box>
</Modal>
);
}
function ConfirmationExample() {
const [open, setOpen] = useState(false);
const [result, setResult] = useState('');
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const handleConfirm = () => {
// Perform the confirmed action
setResult('Action confirmed at ' + new Date().toLocaleTimeString());
};
return (
<Box>
<Button variant="contained" color="error" onClick={handleOpen}>
Delete Item
</Button>
{result && (
<Typography sx={{ mt: 2 }}>
{result}
</Typography>
)}
<ConfirmationModal
open={open}
onClose={handleClose}
onConfirm={handleConfirm}
title="Confirm Deletion"
message="Are you sure you want to delete this item? This action cannot be undone."
confirmText="Delete"
cancelText="Cancel"
severity="error"
/>
</Box>
);
}
export default ConfirmationExample;
This confirmation modal:
- Is reusable and customizable with props for title, message, button text, and severity
- Uses color coding to indicate severity
- Has proper focus management for keyboard users
- Provides clear actions with distinguished primary and secondary buttons
- Returns to the previous UI state if canceled
This pattern is essential for destructive or important actions where you want to prevent accidental clicks.
Step 13: Creating a Multi-Step Modal
For complex workflows, a multi-step modal can guide users through a process:
import React, { useState } from 'react';
import {
Box,
Button,
Modal,
Typography,
Stepper,
Step,
StepLabel,
TextField,
Stack,
FormControlLabel,
Checkbox
} from '@mui/material';
function MultiStepModal() {
const [open, setOpen] = useState(false);
const [activeStep, setActiveStep] = useState(0);
const [formData, setFormData] = useState({
name: '',
email: '',
agreeToTerms: false
});
const steps = ['Personal Info', 'Contact Details', 'Review & Submit'];
const handleOpen = () => {
setOpen(true);
setActiveStep(0);
setFormData({
name: '',
email: '',
agreeToTerms: false
});
};
const handleClose = () => setOpen(false);
const handleNext = () => {
setActiveStep((prevStep) => prevStep + 1);
};
const handleBack = () => {
setActiveStep((prevStep) => prevStep - 1);
};
const handleChange = (event) => {
const { name, value, checked, type } = event.target;
setFormData({
...formData,
[name]: type === 'checkbox' ? checked : value
});
};
const handleSubmit = () => {
// Process the form data
console.log('Form submitted:', formData);
handleClose();
// You would typically send this data to your server here
};
// Determine if the current step is complete
const isStepComplete = () => {
if (activeStep === 0) {
return formData.name.trim() !== '';
}
if (activeStep === 1) {
return formData.email.trim() !== '';
}
if (activeStep === 2) {
return formData.agreeToTerms;
}
return false;
};
// Render the content for the current step
const getStepContent = (step) => {
switch (step) {
case 0:
return (
<Box sx={{ p: 2 }}>
<Typography variant="h6" gutterBottom>
Personal Information
</Typography>
<TextField
fullWidth
label="Full Name"
name="name"
value={formData.name}
onChange={handleChange}
margin="normal"
required
/>
</Box>
);
case 1:
return (
<Box sx={{ p: 2 }}>
<Typography variant="h6" gutterBottom>
Contact Details
</Typography>
<TextField
fullWidth
label="Email Address"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
margin="normal"
required
/>
</Box>
);
case 2:
return (
<Box sx={{ p: 2 }}>
<Typography variant="h6" gutterBottom>
Review Your Information
</Typography>
<Typography>
<strong>Name:</strong> {formData.name}
</Typography>
<Typography>
<strong>Email:</strong> {formData.email}
</Typography>
<FormControlLabel
control={
<Checkbox
name="agreeToTerms"
checked={formData.agreeToTerms}
onChange={handleChange}
required
/>
}
label="I agree to the terms and conditions"
sx={{ mt: 2 }}
/>
</Box>
);
default:
return 'Unknown step';
}
};
return (
<div>
<Button variant="contained" onClick={handleOpen}>
Open Multi-Step Modal
</Button>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="multi-step-modal-title"
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 500,
maxWidth: '90%',
bgcolor: 'background.paper',
boxShadow: 24,
borderRadius: 2,
p: 3,
}}
>
<Typography id="multi-step-modal-title" variant="h5" component="h2" gutterBottom>
Registration Form
</Typography>
<Stepper activeStep={activeStep} sx={{ mb: 3 }}>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
{getStepContent(activeStep)}
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 3 }}>
<Button
variant="outlined"
onClick={activeStep === 0 ? handleClose : handleBack}
>
{activeStep === 0 ? 'Cancel' : 'Back'}
</Button>
<Button
variant="contained"
onClick={activeStep === steps.length - 1 ? handleSubmit : handleNext}
disabled={!isStepComplete()}
>
{activeStep === steps.length - 1 ? 'Submit' : 'Next'}
</Button>
</Box>
</Box>
</Modal>
</div>
);
}
export default MultiStepModal;
This multi-step modal:
- Uses MUI's Stepper component to show progress
- Manages form state across steps
- Validates each step before allowing progression
- Provides back and next navigation
- Shows a summary for review before submission
Multi-step modals are excellent for breaking complex forms into manageable chunks, reducing cognitive load for users.
Common Issues and Solutions
Let's address some common challenges when working with modals.
Preventing Body Scrolling
By default, MUI's Modal prevents the body from scrolling when open. However, if you're experiencing issues, you can control this behavior with the disableScrollLock
prop:
<Modal
open={open}
onClose={handleClose}
disableScrollLock={false} // Default is false, which prevents body scrolling
>
{/* Modal content */}
</Modal>
Handling Modal Inside Modal
Sometimes you need to open a modal from within another modal. Here's how to handle this correctly:
import React, { useState } from 'react';
import { Box, Button, Modal, Typography } from '@mui/material';
function NestedModals() {
const [outerOpen, setOuterOpen] = useState(false);
const [innerOpen, setInnerOpen] = useState(false);
const handleOuterOpen = () => setOuterOpen(true);
const handleOuterClose = () => {
setOuterOpen(false);
setInnerOpen(false); // Also close inner modal when outer is closed
};
const handleInnerOpen = () => setInnerOpen(true);
const handleInnerClose = () => setInnerOpen(false);
const modalStyle = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
bgcolor: 'background.paper',
boxShadow: 24,
borderRadius: 2,
p: 4,
};
return (
<div>
<Button variant="contained" onClick={handleOuterOpen}>
Open Outer Modal
</Button>
{/* Outer Modal */}
<Modal
open={outerOpen}
onClose={handleOuterClose}
aria-labelledby="outer-modal-title"
>
<Box sx={{ ...modalStyle, width: 400 }}>
<Typography id="outer-modal-title" variant="h6" component="h2" gutterBottom>
Outer Modal
</Typography>
<Typography paragraph>
This is the outer modal. You can open another modal from here.
</Typography>
<Button variant="contained" onClick={handleInnerOpen}>
Open Inner Modal
</Button>
</Box>
</Modal>
{/* Inner Modal */}
<Modal
open={innerOpen}
onClose={handleInnerClose}
aria-labelledby="inner-modal-title"
// These props are important for nested modals
disableEnforceFocus
disableAutoFocus
>
<Box sx={{ ...modalStyle, width: 300 }}>
<Typography id="inner-modal-title" variant="h6" component="h2" gutterBottom>
Inner Modal
</Typography>
<Typography paragraph>
This is the inner modal that opens on top of the outer modal.
</Typography>
<Button variant="contained" onClick={handleInnerClose}>
Close
</Button>
</Box>
</Modal>
</div>
);
}
export default NestedModals;
The key to nested modals is using disableEnforceFocus
and disableAutoFocus
on the inner modal to prevent focus management conflicts.
Fixing z-index Issues
If your modal appears behind other elements, you may need to adjust its z-index:
<Modal
open={open}
onClose={handleClose}
sx={{
// Increase z-index if needed
zIndex: (theme) => theme.zIndex.drawer + 1
}}
>
{/* Modal content */}
</Modal>
MUI has predefined z-index levels in the theme that you can reference to maintain proper stacking order.
Performance Optimization
For modals with complex content, you can optimize performance by controlling when the content is mounted:
import React, { useState } from 'react';
import { Box, Button, Modal, Typography } from '@mui/material';
function OptimizedModal() {
const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
return (
<div>
<Button variant="contained" onClick={handleOpen}>
Open Optimized Modal
</Button>
<Modal
open={open}
onClose={handleClose}
// Only mount the modal content when it's open
keepMounted={false}
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
boxShadow: 24,
borderRadius: 2,
p: 4,
}}
>
<Typography variant="h6" component="h2" gutterBottom>
Performance Optimized Modal
</Typography>
<Typography>
This modal doesn't keep its content mounted when closed,
which can improve performance for complex content.
</Typography>
</Box>
</Modal>
</div>
);
}
export default OptimizedModal;
The keepMounted={false}
prop ensures that the modal content is only rendered when the modal is open, which can reduce unnecessary DOM nodes and improve performance.
Best Practices for MUI Modals
Based on my experience, here are some best practices to follow when implementing modals:
1. Keep Modal Content Focused
Modals should serve a specific purpose and contain only the necessary elements. Avoid cramming too much functionality into a single modal.
2. Provide Multiple Ways to Close
Always provide multiple ways to dismiss a modal:
- Close button in the corner
- Cancel/Close button in the actions area
- Clicking outside the modal (for non-critical actions)
- ESC key (unless explicitly disabled)
3. Use Appropriate Sizing
- On desktop, limit modal width to 500-600px for simple forms
- For complex content, consider 80% of viewport width but not more than 1200px
- On mobile, use nearly full width (90-95%) with proper padding
4. Handle Keyboard Navigation
Ensure users can navigate the modal using the keyboard:
- Tab navigation between focusable elements
- Enter to submit forms or activate primary actions
- ESC to close the modal
5. Implement Proper Error Handling
If your modal contains a form, handle errors gracefully:
- Display validation errors inline
- Prevent closing if there are unsaved changes
- Provide clear error messages
6. Consider Animation Timing
- Keep animations brief (150-300ms) to avoid feeling sluggish
- Use consistent animations throughout your application
- Consider reducing animations for users who prefer reduced motion
7. Test on Multiple Devices
Modal behavior can vary across devices and screen sizes:
- Test on desktop, tablet, and mobile
- Ensure content is accessible on all screen sizes
- Verify touch interactions work as expected
Wrapping Up
MUI's Modal component provides a solid foundation for building centered overlay popups in React applications. By understanding its core functionality and customization options, you can create modals that are not only visually appealing but also accessible and user-friendly.
Throughout this guide, we've explored everything from basic implementation to advanced techniques like responsive layouts, animation, accessibility enhancements, and complex use cases. The reusable components and patterns we've covered can be adapted to fit almost any modal requirement in your applications.
Remember that modals should enhance the user experience, not hinder it. By following the best practices outlined here, you can create modal experiences that feel natural, intuitive, and helpful to your users.