How to Use React MUI Backdrop to Build a Loading Overlay for File Upload
File uploads are a common feature in modern web applications, but they can take time to complete. Without proper visual feedback, users might wonder if their upload is working or become frustrated with the apparent lack of response. This is where a loading overlay can significantly improve user experience, and Material UI's Backdrop component offers an elegant solution for implementing this pattern.
In this guide, I'll walk you through creating a polished file upload system with a loading overlay using MUI's Backdrop component. By the end, you'll have a professional, user-friendly file upload interface that clearly communicates upload status to your users.
Learning Objectives
After reading this tutorial, you'll be able to:
- Understand MUI's Backdrop component and its core functionality
- Implement a file upload system with visual loading feedback
- Control Backdrop visibility based on application state
- Style and customize the Backdrop for different visual requirements
- Combine Backdrop with other MUI components for enhanced user interfaces
- Handle edge cases and errors gracefully in your file upload process
- Optimize the performance of your file upload overlay
Understanding MUI Backdrop Component
The Backdrop component in Material UI is designed to provide emphasis on a particular element or action by dimming the background and bringing focus to the foreground content. It's essentially a full-screen semi-transparent layer that sits behind dialogs, drawers, and other components to create a visual hierarchy.
Backdrop is particularly useful for loading states, where you want to prevent user interaction with the page while an operation completes. It creates a clear visual cue that something is happening and the application is working on a task.
Core Props and Configuration
The Backdrop component comes with several key props that control its behavior and appearance:
Prop | Type | Default | Description |
---|---|---|---|
open | boolean | false | Controls whether the backdrop is displayed |
invisible | boolean | false | If true, the backdrop is invisible (no background color) |
transitionDuration | number | appear?: number, enter?: number, exit?: number | - | Duration for the transition, in milliseconds |
component | elementType | 'div' | The component used for the root node |
sx | object | - | The system prop that allows defining system overrides as well as additional CSS styles |
onClick | function | - | Callback fired when the backdrop is clicked |
Basic Usage
Let's start with a simple example of how to use the Backdrop component:
import React, { useState } from 'react';
import { Backdrop, Button, CircularProgress, Box } from '@mui/material';
function SimpleBackdrop() {
const [open, setOpen] = useState(false);
const handleToggle = () => {
setOpen(!open);
};
return (
<Box>
<Button onClick={handleToggle}>Show Backdrop</Button>
<Backdrop
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
open={open}
onClick={handleToggle} >
<CircularProgress color="inherit" />
</Backdrop>
</Box>
);
}
In this basic example, the Backdrop is controlled by the open
state. When the button is clicked, the Backdrop appears with a CircularProgress component centered on the screen. Clicking anywhere on the Backdrop will close it.
Customization Options
Backdrop can be customized in several ways:
- Styling with the sx prop: The most direct way to style the Backdrop component is using the
sx
prop:
<Backdrop
sx={{
color: '#fff',
backgroundColor: 'rgba(0, 0, 0, 0.8)', // More opaque background
zIndex: (theme) => theme.zIndex.drawer + 1,
backdropFilter: 'blur(3px)', // Adds a blur effect
}}
open={open}
/>
- Using theme customization: For application-wide styling, you can override the Backdrop component in your theme:
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
components: {
MuiBackdrop: {
styleOverrides: {
root: {
backgroundColor: 'rgba(0, 0, 0, 0.7)',
},
},
},
},
});
- Styled API: For more complex styling needs, you can use the styled API:
import { styled } from '@mui/material/styles';
import Backdrop from '@mui/material/Backdrop';
const CustomBackdrop = styled(Backdrop)(({ theme }) => ({
zIndex: theme.zIndex.drawer + 1,
color: '#fff',
backgroundColor: 'rgba(0, 0, 50, 0.8)', // Bluish background
backdropFilter: 'blur(4px)',
}));
Accessibility Considerations
When using the Backdrop component, especially for loading states, it's important to consider accessibility:
-
Keyboard navigation: When the Backdrop is open, users should not be able to navigate to elements behind it.
-
Screen readers: Use appropriate ARIA attributes to communicate the loading state to screen readers.
-
Focus management: Ensure that focus is properly managed when the Backdrop appears and disappears.
<Backdrop
open={loading}
sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}
// Adding accessibility attributes
aria-live="polite"
aria-busy={loading}
>
<Box role="status">
<CircularProgress aria-label="Loading" />
<span>Uploading files...</span>
</Box>
</Backdrop>
Building a File Upload System with Backdrop
Now that we understand the Backdrop component, let's build a complete file upload system with a loading overlay. We'll break this down into manageable steps.
Step 1: Setting Up the Project
First, let's set up a new React project and install the necessary dependencies:
// If you're starting from scratch
npx create-react-app file-upload-with-backdrop
cd file-upload-with-backdrop
// Install Material UI
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material
// For file uploads, we'll use axios
npm install axios
Step 2: Creating the File Upload Component Structure
Let's create a basic structure for our file upload component:
import React, { useState } from 'react';
import {
Box,
Button,
Typography,
Backdrop,
CircularProgress,
Paper,
Stack,
Alert,
} from '@mui/material';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import axios from 'axios';
function FileUploadWithBackdrop() {
const [file, setFile] = useState(null);
const [loading, setLoading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
// We'll implement these functions next
const handleFileChange = (event) => {};
const handleUpload = async () => {};
const handleReset = () => {};
return (
<Paper elevation={3} sx={{ p: 3, maxWidth: 500, mx: 'auto', mt: 4 }}>
{/* Component UI will go here */}
</Paper>
);
}
export default FileUploadWithBackdrop;
In this structure, we're setting up state variables to track:
- The selected file
- Loading state (to control the Backdrop)
- Upload progress
- Error messages
- Success state
Step 3: Implementing the File Selection UI
Now, let's implement the file selection UI:
// Inside the return statement of FileUploadWithBackdrop
return (
<Paper elevation={3} sx={{ p: 3, maxWidth: 500, mx: 'auto', mt: 4 }}>
<Typography variant="h5" gutterBottom>
File Upload with Loading Overlay
</Typography>
<Box
sx={{
border: '2px dashed #ccc',
borderRadius: 2,
p: 3,
mb: 3,
textAlign: 'center',
cursor: 'pointer',
'&:hover': {
borderColor: 'primary.main',
},
}}
onClick={() => document.getElementById('file-input').click()}
>
<input
id="file-input"
type="file"
onChange={handleFileChange}
style={{ display: 'none' }}
/>
<CloudUploadIcon sx={{ fontSize: 48, color: 'primary.main', mb: 1 }} />
<Typography>
{file ? file.name : 'Click to select or drop a file here'}
</Typography>
{file && (
<Typography variant="body2" color="textSecondary">
Size: {(file.size / 1024).toFixed(2)} KB
</Typography>
)}
</Box>
<Stack direction="row" spacing={2} justifyContent="flex-end">
{file && (
<Button variant="outlined" onClick={handleReset}>
Reset
</Button>
)}
<Button
variant="contained"
disabled={!file || loading}
onClick={handleUpload}
>
Upload
</Button>
</Stack>
{error && (
<Alert severity="error" sx={{ mt: 2 }}>
{error}
</Alert>
)}
{success && (
<Alert severity="success" sx={{ mt: 2 }}>
File uploaded successfully!
</Alert>
)}
</Paper>
);
This creates a drop zone for file selection, displays file information once selected, and provides buttons for uploading and resetting.
Step 4: Implementing the File Handling Logic
Next, let's implement the file handling functions:
const handleFileChange = (event) => {
const selectedFile = event.target.files[0];
if (selectedFile) {
setFile(selectedFile);
setError(null);
setSuccess(false);
}
};
const handleReset = () => {
setFile(null);
setError(null);
setSuccess(false);
setUploadProgress(0);
};
const handleUpload = async () => {
if (!file) return;
setLoading(true);
setError(null);
setUploadProgress(0);
// Create a FormData object to send the file
const formData = new FormData();
formData.append('file', file);
try {
// Replace with your actual API endpoint
await axios.post('https://your-api-endpoint.com/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
setUploadProgress(percentCompleted);
},
});
setSuccess(true);
// Optionally reset the file after successful upload
// setFile(null);
} catch (err) {
console.error('Upload failed:', err);
setError(
err.response?.data?.message ||
'Upload failed. Please try again.'
);
} finally {
setLoading(false);
}
};
These functions handle:
- Selecting a file and updating state
- Resetting the component state
- Uploading the file with progress tracking
Step 5: Adding the Backdrop Loading Overlay
Now, let's add the Backdrop component to show the loading state during file upload:
// Add this to your imports if not already there
import { Box, CircularProgress, Typography } from '@mui/material';
// Add this to your component return, after the Paper component
<Backdrop
sx={{
color: "#fff",
zIndex: (theme) => theme.zIndex.drawer + 1,
flexDirection: "column",
}}
open={loading}
>
<CircularProgress
color="inherit"
variant="determinate"
value={uploadProgress}
size={60}
thickness={4}
/>
<Box sx={{ mt: 2 }}>
<Typography variant="h6">Uploading: {uploadProgress}%</Typography>
</Box>
</Backdrop>
This Backdrop will appear when loading
is true and display a progress indicator that updates as the file uploads.
Step 6: Putting It All Together
Let's combine all the pieces to create our complete file upload component with a loading overlay:
import React, { useState } from 'react';
import {
Box,
Button,
Typography,
Backdrop,
CircularProgress,
Paper,
Stack,
Alert,
} from '@mui/material';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import axios from 'axios';
function FileUploadWithBackdrop() {
const [file, setFile] = useState(null);
const [loading, setLoading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const handleFileChange = (event) => {
const selectedFile = event.target.files[0];
if (selectedFile) {
setFile(selectedFile);
setError(null);
setSuccess(false);
}
};
const handleReset = () => {
setFile(null);
setError(null);
setSuccess(false);
setUploadProgress(0);
};
const handleUpload = async () => {
if (!file) return;
setLoading(true);
setError(null);
setUploadProgress(0);
// Create a FormData object to send the file
const formData = new FormData();
formData.append('file', file);
try {
// For demo purposes, we'll simulate a delay and progress
// Replace this with your actual API call in production
await new Promise((resolve) => {
let progress = 0;
const interval = setInterval(() => {
progress += 5;
setUploadProgress(progress);
if (progress >= 100) {
clearInterval(interval);
resolve();
}
}, 300);
});
setSuccess(true);
} catch (err) {
console.error('Upload failed:', err);
setError('Upload failed. Please try again.');
} finally {
setLoading(false);
}
};
return (
<Box>
<Paper elevation={3} sx={{ p: 3, maxWidth: 500, mx: 'auto', mt: 4 }}>
<Typography variant="h5" gutterBottom>
File Upload with Loading Overlay
</Typography>
<Box
sx={{
border: '2px dashed #ccc',
borderRadius: 2,
p: 3,
mb: 3,
textAlign: 'center',
cursor: 'pointer',
'&:hover': {
borderColor: 'primary.main',
},
}}
onClick={() => document.getElementById('file-input').click()}
>
<input
id="file-input"
type="file"
onChange={handleFileChange}
style={{ display: 'none' }}
/>
<CloudUploadIcon sx={{ fontSize: 48, color: 'primary.main', mb: 1 }} />
<Typography>
{file ? file.name : 'Click to select or drop a file here'}
</Typography>
{file && (
<Typography variant="body2" color="textSecondary">
Size: {(file.size / 1024).toFixed(2)} KB
</Typography>
)}
</Box>
<Stack direction="row" spacing={2} justifyContent="flex-end">
{file && (
<Button variant="outlined" onClick={handleReset}>
Reset
</Button>
)}
<Button
variant="contained"
disabled={!file || loading}
onClick={handleUpload}
>
Upload
</Button>
</Stack>
{error && (
<Alert severity="error" sx={{ mt: 2 }}>
{error}
</Alert>
)}
{success && (
<Alert severity="success" sx={{ mt: 2 }}>
File uploaded successfully!
</Alert>
)}
</Paper>
<Backdrop
sx={{
color: '#fff',
zIndex: (theme) => theme.zIndex.drawer + 1,
flexDirection: 'column',
}}
open={loading}
>
<CircularProgress
color="inherit"
variant="determinate"
value={uploadProgress}
size={60}
thickness={4}
/>
<Box sx={{ mt: 2 }}>
<Typography variant="h6">Uploading: {uploadProgress}%</Typography>
</Box>
</Backdrop>
</Box>
);
}
export default FileUploadWithBackdrop;
For demonstration purposes, I've replaced the actual API call with a simulated progress update. In a real application, you would replace this with your actual API endpoint.
Advanced Backdrop Features for File Upload
Now that we have a basic file upload system with a loading overlay, let's explore some advanced features and customizations.
Adding Drag and Drop Support
Let's enhance our component with drag and drop support:
import React, { useState, useRef } from 'react';
// ... other imports
function FileUploadWithBackdrop() {
// ... existing state variables
const dropAreaRef = useRef(null);
// Add these new functions
const handleDragOver = (e) => {
e.preventDefault();
e.stopPropagation();
if (dropAreaRef.current) {
dropAreaRef.current.style.borderColor = '#1976d2'; // Primary color
dropAreaRef.current.style.backgroundColor = 'rgba(25, 118, 210, 0.04)';
}
};
const handleDragLeave = (e) => {
e.preventDefault();
e.stopPropagation();
if (dropAreaRef.current) {
dropAreaRef.current.style.borderColor = '#ccc';
dropAreaRef.current.style.backgroundColor = 'transparent';
}
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
if (dropAreaRef.current) {
dropAreaRef.current.style.borderColor = '#ccc';
dropAreaRef.current.style.backgroundColor = 'transparent';
}
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
const droppedFile = e.dataTransfer.files[0];
setFile(droppedFile);
setError(null);
setSuccess(false);
}
};
// ... existing functions
// Update your drop area Box component
return (
<Box>
<Paper elevation={3} sx={{ p: 3, maxWidth: 500, mx: 'auto', mt: 4 }}>
{/* ... */}
<Box
ref={dropAreaRef}
sx={{
border: '2px dashed #ccc',
borderRadius: 2,
p: 3,
mb: 3,
textAlign: 'center',
cursor: 'pointer',
transition: 'all 0.2s ease',
'&:hover': {
borderColor: 'primary.main',
},
}}
onClick={() => document.getElementById('file-input').click()}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop} >
{/* ... existing content */}
</Box>
{/* ... */}
</Paper>
{/* ... Backdrop component */}
</Box>
);
}
This enhancement adds visual feedback during drag operations and handles file drops.
Supporting Multiple File Uploads
Let's modify our component to support multiple file uploads:
import React, { useState } from 'react';
// ... other imports
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import ListItemIcon from '@mui/material/ListItemIcon';
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
import DeleteIcon from '@mui/icons-material/Delete';
import IconButton from '@mui/material/IconButton';
function MultipleFileUploadWithBackdrop() {
const [files, setFiles] = useState([]);
const [loading, setLoading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const handleFileChange = (event) => {
if (event.target.files) {
// Convert FileList to Array and append to existing files
const newFiles = Array.from(event.target.files);
setFiles((prevFiles) => [...prevFiles, ...newFiles]);
setError(null);
setSuccess(false);
}
};
const handleRemoveFile = (index) => {
setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index));
};
const handleReset = () => {
setFiles([]);
setError(null);
setSuccess(false);
setUploadProgress(0);
};
const handleUpload = async () => {
if (files.length === 0) return;
setLoading(true);
setError(null);
setUploadProgress(0);
// Create a FormData object to send multiple files
const formData = new FormData();
files.forEach((file, index) => {
formData.append(`file-${index}`, file);
});
try {
// Simulate upload progress for demo
await new Promise((resolve) => {
let progress = 0;
const interval = setInterval(() => {
progress += 5;
setUploadProgress(progress);
if (progress >= 100) {
clearInterval(interval);
resolve();
}
}, 300);
});
setSuccess(true);
} catch (err) {
console.error('Upload failed:', err);
setError('Upload failed. Please try again.');
} finally {
setLoading(false);
}
};
const totalSize = files.reduce((total, file) => total + file.size, 0);
const formattedTotalSize = (totalSize / 1024).toFixed(2);
return (
<Box>
<Paper elevation={3} sx={{ p: 3, maxWidth: 500, mx: 'auto', mt: 4 }}>
<Typography variant="h5" gutterBottom>
Multiple File Upload with Loading Overlay
</Typography>
<Box
sx={{
border: '2px dashed #ccc',
borderRadius: 2,
p: 3,
mb: 3,
textAlign: 'center',
cursor: 'pointer',
'&:hover': {
borderColor: 'primary.main',
},
}}
onClick={() => document.getElementById('file-input').click()}
>
<input
id="file-input"
type="file"
multiple
onChange={handleFileChange}
style={{ display: 'none' }}
/>
<CloudUploadIcon sx={{ fontSize: 48, color: 'primary.main', mb: 1 }} />
<Typography>
Click to select or drop files here
</Typography>
{files.length > 0 && (
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
{files.length} files selected ({formattedTotalSize} KB)
</Typography>
)}
</Box>
{files.length > 0 && (
<List dense sx={{ mb: 2, maxHeight: 200, overflow: 'auto' }}>
{files.map((file, index) => (
<ListItem
key={index}
secondaryAction={
<IconButton edge="end" onClick={() => handleRemoveFile(index)}>
<DeleteIcon />
</IconButton>
}
>
<ListItemIcon>
<InsertDriveFileIcon />
</ListItemIcon>
<ListItemText
primary={file.name}
secondary={`${(file.size / 1024).toFixed(2)} KB`}
/>
</ListItem>
))}
</List>
)}
<Stack direction="row" spacing={2} justifyContent="flex-end">
{files.length > 0 && (
<Button variant="outlined" onClick={handleReset}>
Reset
</Button>
)}
<Button
variant="contained"
disabled={files.length === 0 || loading}
onClick={handleUpload}
>
Upload {files.length > 0 ? `${files.length} Files` : ''}
</Button>
</Stack>
{error && (
<Alert severity="error" sx={{ mt: 2 }}>
{error}
</Alert>
)}
{success && (
<Alert severity="success" sx={{ mt: 2 }}>
All files uploaded successfully!
</Alert>
)}
</Paper>
<Backdrop
sx={{
color: '#fff',
zIndex: (theme) => theme.zIndex.drawer + 1,
flexDirection: 'column',
}}
open={loading}
>
<CircularProgress
color="inherit"
variant="determinate"
value={uploadProgress}
size={60}
thickness={4}
/>
<Box sx={{ mt: 2 }}>
<Typography variant="h6">Uploading: {uploadProgress}%</Typography>
<Typography variant="body2" sx={{ mt: 1 }}>
Uploading {files.length} files ({formattedTotalSize} KB)
</Typography>
</Box>
</Backdrop>
</Box>
);
}
This enhanced version supports multiple file selection, displays a list of selected files, and allows individual files to be removed before upload.
Creating a Customized Backdrop for Different Upload Stages
Let's create a more sophisticated Backdrop that changes its display based on the upload stage:
import React, { useState } from 'react';
// ... other imports
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import ErrorIcon from '@mui/icons-material/Error';
import { keyframes } from '@emotion/react';
// Define a fade-in animation
const fadeIn = keyframes`
from {
opacity: 0;
}
to {
opacity: 1;
}
`;
function AdvancedFileUploadWithBackdrop() {
// ... existing state variables
const [uploadStage, setUploadStage] = useState('idle'); // 'idle', 'uploading', 'processing', 'success', 'error'
const handleUpload = async () => {
if (!file) return;
setLoading(true);
setUploadStage('uploading');
setUploadProgress(0);
const formData = new FormData();
formData.append('file', file);
try {
// Simulate upload progress
await new Promise((resolve) => {
let progress = 0;
const interval = setInterval(() => {
progress += 10;
setUploadProgress(progress);
if (progress >= 100) {
clearInterval(interval);
resolve();
}
}, 300);
});
// Simulate server processing
setUploadStage('processing');
await new Promise((resolve) => setTimeout(resolve, 2000));
// Simulate success
setUploadStage('success');
setSuccess(true);
// Keep success message visible for a moment
await new Promise((resolve) => setTimeout(resolve, 1500));
} catch (err) {
setUploadStage('error');
setError('Upload failed. Please try again.');
// Keep error message visible for a moment
await new Promise((resolve) => setTimeout(resolve, 1500));
} finally {
setLoading(false);
setUploadStage('idle');
}
};
// ... other functions
// Custom backdrop content based on upload stage
const renderBackdropContent = () => {
switch (uploadStage) {
case 'uploading':
return (
<>
<CircularProgress
color="inherit"
variant="determinate"
value={uploadProgress}
size={60}
thickness={4}
/>
<Box sx={{ mt: 2, animation: `${fadeIn} 0.3s ease-in` }}>
<Typography variant="h6">Uploading: {uploadProgress}%</Typography>
<Typography variant="body2" sx={{ mt: 1, textAlign: 'center' }}>
Please don't close this window
</Typography>
</Box>
</>
);
case 'processing':
return (
<>
<CircularProgress
color="inherit"
size={60}
/>
<Box sx={{ mt: 2, animation: `${fadeIn} 0.3s ease-in` }}>
<Typography variant="h6">Processing your file...</Typography>
<Typography variant="body2" sx={{ mt: 1, textAlign: 'center' }}>
This may take a moment
</Typography>
</Box>
</>
);
case 'success':
return (
<Box sx={{ textAlign: 'center', animation: `${fadeIn} 0.3s ease-in` }}>
<CheckCircleIcon sx={{ fontSize: 60, color: '#4caf50' }} />
<Typography variant="h6" sx={{ mt: 2 }}>
Upload Complete!
</Typography>
</Box>
);
case 'error':
return (
<Box sx={{ textAlign: 'center', animation: `${fadeIn} 0.3s ease-in` }}>
<ErrorIcon sx={{ fontSize: 60, color: '#f44336' }} />
<Typography variant="h6" sx={{ mt: 2 }}>
Upload Failed
</Typography>
<Typography variant="body2" sx={{ mt: 1 }}>
Please try again
</Typography>
</Box>
);
default:
return null;
}
};
return (
<Box>
{/* ... existing UI */}
<Backdrop
sx={{
color: '#fff',
zIndex: (theme) => theme.zIndex.drawer + 1,
flexDirection: 'column',
backdropFilter: 'blur(3px)',
}}
open={loading}
>
{renderBackdropContent()}
</Backdrop>
</Box>
);
}
This advanced version shows different content in the Backdrop based on the current stage of the upload process, providing more detailed feedback to the user.
Integrating with Form Libraries
In real-world applications, file uploads are often part of larger forms. Let's see how to integrate our component with a popular form library like Formik:
import React, { useState } from 'react';
import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';
import {
Box,
Button,
Typography,
Backdrop,
CircularProgress,
Paper,
Stack,
Alert,
TextField,
} from '@mui/material';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
// Define validation schema
const FileUploadSchema = Yup.object().shape({
name: Yup.string()
.required('Name is required'),
description: Yup.string()
.required('Description is required'),
// We'll validate the file in our component since Yup doesn't handle it directly
});
function FormikFileUploadWithBackdrop() {
const [file, setFile] = useState(null);
const [fileError, setFileError] = useState(null);
const [loading, setLoading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [submitResult, setSubmitResult] = useState(null);
const handleFileChange = (event) => {
const selectedFile = event.target.files[0];
if (selectedFile) {
// Validate file type and size
const validTypes = ['image/jpeg', 'image/png', 'application/pdf'];
const maxSize = 5 _ 1024 _ 1024; // 5MB
if (!validTypes.includes(selectedFile.type)) {
setFileError('Invalid file type. Please upload a JPEG, PNG or PDF file.');
setFile(null);
return;
}
if (selectedFile.size > maxSize) {
setFileError('File is too large. Maximum size is 5MB.');
setFile(null);
return;
}
setFile(selectedFile);
setFileError(null);
}
};
const handleSubmit = async (values, { resetForm }) => {
if (!file) {
setFileError('Please select a file to upload');
return;
}
setLoading(true);
setUploadProgress(0);
// Create a FormData object to send the file and form values
const formData = new FormData();
formData.append('file', file);
formData.append('name', values.name);
formData.append('description', values.description);
try {
// Simulate upload progress for demo
await new Promise((resolve) => {
let progress = 0;
const interval = setInterval(() => {
progress += 5;
setUploadProgress(progress);
if (progress >= 100) {
clearInterval(interval);
resolve();
}
}, 300);
});
// Simulate a successful response
setSubmitResult({
type: 'success',
message: 'Form submitted and file uploaded successfully!'
});
resetForm();
setFile(null);
} catch (err) {
console.error('Submission failed:', err);
setSubmitResult({
type: 'error',
message: 'Submission failed. Please try again.'
});
} finally {
setLoading(false);
}
};
return (
<Box>
<Paper elevation={3} sx={{ p: 3, maxWidth: 600, mx: 'auto', mt: 4 }}>
<Typography variant="h5" gutterBottom>
File Upload Form
</Typography>
<Formik
initialValues={{
name: '',
description: '',
}}
validationSchema={FileUploadSchema}
onSubmit={handleSubmit}
>
{({ errors, touched }) => (
<Form>
<Box sx={{ mb: 3 }}>
<Field
as={TextField}
fullWidth
margin="normal"
name="name"
label="Name"
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
/>
<Field
as={TextField}
fullWidth
margin="normal"
name="description"
label="Description"
multiline
rows={4}
error={touched.description && Boolean(errors.description)}
helperText={touched.description && errors.description}
/>
</Box>
<Box
sx={{
border: '2px dashed #ccc',
borderRadius: 2,
p: 3,
mb: 3,
textAlign: 'center',
cursor: 'pointer',
'&:hover': {
borderColor: 'primary.main',
},
}}
onClick={() => document.getElementById('file-input').click()}
>
<input
id="file-input"
type="file"
onChange={handleFileChange}
style={{ display: 'none' }}
/>
<CloudUploadIcon sx={{ fontSize: 48, color: 'primary.main', mb: 1 }} />
<Typography>
{file ? file.name : 'Click to select a file'}
</Typography>
{file && (
<Typography variant="body2" color="textSecondary">
Size: {(file.size / 1024).toFixed(2)} KB
</Typography>
)}
</Box>
{fileError && (
<Alert severity="error" sx={{ mb: 2 }}>
{fileError}
</Alert>
)}
<Button
type="submit"
variant="contained"
fullWidth
disabled={loading}
>
Submit Form and Upload File
</Button>
</Form>
)}
</Formik>
{submitResult && (
<Alert severity={submitResult.type} sx={{ mt: 2 }}>
{submitResult.message}
</Alert>
)}
</Paper>
<Backdrop
sx={{
color: '#fff',
zIndex: (theme) => theme.zIndex.drawer + 1,
flexDirection: 'column',
}}
open={loading}
>
<CircularProgress
color="inherit"
variant="determinate"
value={uploadProgress}
size={60}
thickness={4}
/>
<Box sx={{ mt: 2 }}>
<Typography variant="h6">Uploading: {uploadProgress}%</Typography>
<Typography variant="body2" sx={{ mt: 1 }}>
Submitting form data and uploading file...
</Typography>
</Box>
</Backdrop>
</Box>
);
}
This example integrates file upload with Formik form handling, including validation for both form fields and the uploaded file.
Best Practices and Common Issues
When implementing file uploads with a Backdrop loading overlay, there are several best practices to follow and common issues to be aware of.
Performance Considerations
-
File Size Limitations:
- Always set reasonable file size limits to prevent performance issues
- Validate file sizes on the client side before attempting to upload
-
Image Optimization:
- Consider compressing images before upload for faster transfers
- You can use libraries like
browser-image-compression
for client-side compression:
import imageCompression from 'browser-image-compression';
const compressImage = async (imageFile) => {
const options = {
maxSizeMB: 1, // Maximum size in MB
maxWidthOrHeight: 1920, // Maximum width or height
useWebWorker: true, // Use web worker for better performance
};
try {
const compressedFile = await imageCompression(imageFile, options);
console.log('Original size:', imageFile.size / 1024 / 1024, 'MB');
console.log('Compressed size:', compressedFile.size / 1024 / 1024, 'MB');
return compressedFile;
} catch (error) {
console.error('Error compressing image:', error);
return imageFile; // Return original if compression fails
}
};
- Chunked Uploads:
- For large files, consider implementing chunked uploads to improve reliability
- This allows resuming uploads if they're interrupted
Error Handling
- Network Failures:
- Always handle network failures gracefully
- Provide clear error messages and retry options
const handleUpload = async () => {
if (!file) return;
setLoading(true);
setError(null);
let retryCount = 0;
const maxRetries = 3;
const attemptUpload = async () => {
try {
const formData = new FormData();
formData.append('file', file);
await axios.post('https://your-api-endpoint.com/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
setUploadProgress(percentCompleted);
},
});
setSuccess(true);
} catch (err) {
if (retryCount < maxRetries && err.message.includes('network')) {
retryCount++;
setError(`Network error. Retrying (${retryCount}/${maxRetries})...`);
return attemptUpload(); // Retry
}
throw err; // Rethrow if we can't retry
}
};
try {
await attemptUpload();
} catch (err) {
console.error('Upload failed:', err);
setError('Upload failed after multiple attempts. Please check your connection and try again.');
} finally {
setLoading(false);
}
};
- Server Errors:
- Handle different HTTP status codes appropriately
- Display meaningful error messages based on the server response
Accessibility Improvements
- Keyboard Navigation:
- Ensure your file upload component is fully accessible via keyboard
// Make the drop zone accessible via keyboard
<Box
role="button"
tabIndex={0}
aria-label="Click to select a file or press Enter key"
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
document.getElementById('file-input').click();
}
}}
// ...other props
>
{/* Content */}
</Box>
- Screen Reader Support:
- Use ARIA attributes to improve screen reader support for the Backdrop
<Backdrop
sx={{ /* styles */ }}
open={loading}
aria-live="polite"
aria-busy={loading}
>
<div role="status" aria-label={`Uploading file, ${uploadProgress} percent complete`}>
<CircularProgress /* props */ />
<Typography>Uploading: {uploadProgress}%</Typography>
</div>
</Backdrop>
Common Issues and Solutions
- Issue: Backdrop doesn't appear above other components Solution: Ensure the Backdrop has a higher z-index than other components:
<Backdrop
sx={{
zIndex: (theme) => theme.zIndex.drawer + 1, // Ensure this is high enough
// Other styles
}}
open={loading}
>
{/* Content */}
</Backdrop>
- Issue: Upload progress isn't accurate Solution: Use the correct event properties for progress calculation:
axios.post(url, formData, {
onUploadProgress: (progressEvent) => {
// Check if total is available (might not be in some browsers)
if (progressEvent.total) {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
setUploadProgress(percentCompleted);
} else {
// Fall back to indeterminate progress
setUploadProgress(-1); // Use this to show indeterminate progress
}
},
});
- Issue: File input doesn't reset after upload Solution: Create a ref for the file input and reset it directly:
import React, { useRef } from 'react';
function FileUpload() {
const fileInputRef = useRef(null);
// ...other state and functions
const handleReset = () => {
setFile(null);
setError(null);
setSuccess(false);
// Reset the file input element
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
return (
// ...
<input
ref={fileInputRef}
id="file-input"
type="file"
onChange={handleFileChange}
style={{ display: 'none' }}
/>
// ...
);
}
Creating a Reusable File Upload Component
Let's create a reusable file upload component with a Backdrop that you can use across your application:
import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import {
Box,
Typography,
Backdrop,
CircularProgress,
Button,
Alert,
Paper,
} from '@mui/material';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
const FileUploadWithBackdrop = ({
endpoint,
maxFileSizeMB = 5,
acceptedFileTypes = ['image/*', 'application/pdf'],
onUploadSuccess,
onUploadError,
buttonText = 'Upload',
dropzoneText = 'Click or drag to upload file',
sx = {},
}) => {
const [file, setFile] = useState(null);
const [loading, setLoading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const fileInputRef = useRef(null);
const dropAreaRef = useRef(null);
const maxSizeBytes = maxFileSizeMB _ 1024 _ 1024;
const validateFile = (selectedFile) => {
// Check file size
if (selectedFile.size > maxSizeBytes) {
return `File is too large. Maximum size is ${maxFileSizeMB}MB.`;
}
// Check file type
const fileTypeAccepted = acceptedFileTypes.some(type => {
// Handle wildcard types like 'image/*'
if (type.endsWith('/*')) {
const generalType = type.split('/')[0];
return selectedFile.type.startsWith(`${generalType}/`);
}
return selectedFile.type === type;
});
if (!fileTypeAccepted) {
return `Invalid file type. Accepted types: ${acceptedFileTypes.join(', ')}`;
}
return null; // No error
};
const handleFileChange = (event) => {
const selectedFile = event.target.files?.[0];
if (!selectedFile) return;
const validationError = validateFile(selectedFile);
if (validationError) {
setError(validationError);
setFile(null);
return;
}
setFile(selectedFile);
setError(null);
setSuccess(false);
};
const handleDragOver = (e) => {
e.preventDefault();
e.stopPropagation();
if (dropAreaRef.current) {
dropAreaRef.current.style.borderColor = '#1976d2';
dropAreaRef.current.style.backgroundColor = 'rgba(25, 118, 210, 0.04)';
}
};
const handleDragLeave = (e) => {
e.preventDefault();
e.stopPropagation();
if (dropAreaRef.current) {
dropAreaRef.current.style.borderColor = '#ccc';
dropAreaRef.current.style.backgroundColor = 'transparent';
}
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
if (dropAreaRef.current) {
dropAreaRef.current.style.borderColor = '#ccc';
dropAreaRef.current.style.backgroundColor = 'transparent';
}
const droppedFile = e.dataTransfer.files?.[0];
if (!droppedFile) return;
const validationError = validateFile(droppedFile);
if (validationError) {
setError(validationError);
setFile(null);
return;
}
setFile(droppedFile);
setError(null);
setSuccess(false);
};
const handleReset = () => {
setFile(null);
setError(null);
setSuccess(false);
setUploadProgress(0);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const handleUpload = async () => {
if (!file || !endpoint) return;
setLoading(true);
setError(null);
setUploadProgress(0);
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch(endpoint, {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error(`Server responded with ${response.status}: ${response.statusText}`);
}
const data = await response.json();
setSuccess(true);
if (onUploadSuccess) {
onUploadSuccess(data);
}
} catch (err) {
console.error('Upload failed:', err);
const errorMessage = err.message || 'Upload failed. Please try again.';
setError(errorMessage);
if (onUploadError) {
onUploadError(errorMessage);
}
} finally {
setLoading(false);
}
};
return (
<Paper
elevation={3}
sx={{
p: 3,
...sx
}} >
<Box
ref={dropAreaRef}
sx={{
border: '2px dashed #ccc',
borderRadius: 2,
p: 3,
mb: 3,
textAlign: 'center',
cursor: 'pointer',
transition: 'all 0.2s ease',
'&:hover': {
borderColor: 'primary.main',
},
}}
onClick={() => fileInputRef.current?.click()}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
role="button"
tabIndex={0}
aria-label="Click to select a file or drop it here"
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
fileInputRef.current?.click();
}
}} >
<input
ref={fileInputRef}
type="file"
onChange={handleFileChange}
style={{ display: 'none' }}
accept={acceptedFileTypes.join(',')}
/>
<CloudUploadIcon sx={{ fontSize: 48, color: 'primary.main', mb: 1 }} />
<Typography>
{file ? file.name : dropzoneText}
</Typography>
{file && (
<Typography variant="body2" color="textSecondary">
Size: {(file.size / 1024 / 1024).toFixed(2)} MB
</Typography>
)}
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
{file && (
<Button variant="outlined" onClick={handleReset}>
Reset
</Button>
)}
<Button
variant="contained"
disabled={!file || loading}
onClick={handleUpload}
sx={{ ml: 'auto' }}
>
{buttonText}
</Button>
</Box>
{error && (
<Alert severity="error" sx={{ mt: 2 }}>
{error}
</Alert>
)}
{success && (
<Alert
severity="success"
sx={{ mt: 2 }}
icon={<CheckCircleIcon fontSize="inherit" />}
>
File uploaded successfully!
</Alert>
)}
<Backdrop
sx={{
color: '#fff',
zIndex: (theme) => theme.zIndex.drawer + 1,
flexDirection: 'column',
}}
open={loading}
>
<CircularProgress
color="inherit"
variant="determinate"
value={uploadProgress || 100} // Show indeterminate if no progress info
size={60}
thickness={4}
/>
<Box sx={{ mt: 2 }}>
<Typography variant="h6">
{uploadProgress ? `Uploading: ${uploadProgress}%` : 'Uploading...'}
</Typography>
</Box>
</Backdrop>
</Paper>
);
};
FileUploadWithBackdrop.propTypes = {
endpoint: PropTypes.string.isRequired,
maxFileSizeMB: PropTypes.number,
acceptedFileTypes: PropTypes.arrayOf(PropTypes.string),
onUploadSuccess: PropTypes.func,
onUploadError: PropTypes.func,
buttonText: PropTypes.string,
dropzoneText: PropTypes.string,
sx: PropTypes.object,
};
export default FileUploadWithBackdrop;
This reusable component can be used throughout your application with different configurations:
import FileUploadWithBackdrop from './FileUploadWithBackdrop';
function App() {
const handleUploadSuccess = (data) => {
console.log('Upload successful:', data);
// Do something with the uploaded file data
};
return (
<div>
<h1>Image Upload</h1>
<FileUploadWithBackdrop
endpoint="https://api.example.com/upload-image"
maxFileSizeMB={2}
acceptedFileTypes={['image/jpeg', 'image/png']}
onUploadSuccess={handleUploadSuccess}
buttonText="Upload Image"
dropzoneText="Click or drag to upload an image"
sx={{ maxWidth: 500, mx: 'auto' }}
/>
<h1>Document Upload</h1>
<FileUploadWithBackdrop
endpoint="https://api.example.com/upload-document"
maxFileSizeMB={10}
acceptedFileTypes={['application/pdf', 'application/msword']}
buttonText="Upload Document"
dropzoneText="Click or drag to upload a document"
sx={{ maxWidth: 500, mx: 'auto', mt: 4 }}
/>
</div>
);
}
Wrapping Up
In this comprehensive guide, we've explored how to create a file upload system with a loading overlay using MUI's Backdrop component. We started with the basics of the Backdrop component and gradually built up to a fully-featured, reusable file upload component.
By implementing a loading overlay with Backdrop, you provide users with clear visual feedback during file uploads, improving the overall user experience of your application. We've covered customization options, accessibility considerations, error handling, and performance optimizations to help you build a robust file upload system.
With the knowledge gained from this guide, you can now confidently implement sophisticated file upload interfaces with appropriate loading states that keep your users informed and engaged throughout the upload process.