Menu

Building a Step-Based File Upload Progress with React MUI

When developing modern web applications, providing visual feedback during file uploads is crucial for a good user experience. Users want to know how their upload is progressing, especially for larger files. Material UI (MUI) offers several progress components that can be leveraged to create intuitive and visually appealing upload indicators.

In this comprehensive guide, I'll walk you through building a step-based file upload progress system using React MUI's Progress components. By the end of this tutorial, you'll understand how to implement a robust file upload interface with real-time progress tracking, error handling, and a polished user experience.

What You'll Learn

  • Understanding MUI's various Progress components and their use cases
  • Building a multi-step file upload progress indicator
  • Implementing real-time progress updates with axios interceptors
  • Handling upload states (pending, uploading, success, error)
  • Customizing Progress components with MUI theming and styling
  • Creating accessible progress indicators
  • Best practices for file upload UX

Deep Dive into MUI Progress Components

Before we start building, let's understand the Progress components offered by MUI. Material UI provides two main types of progress indicators: determinate and indeterminate.

Linear Progress Component

The LinearProgress component is a horizontal progress indicator that represents the completion of a process. It's perfect for file uploads as it clearly shows the percentage of completion.

Key Props

PropTypeDefaultDescription
colorstring'primary'The color of the component. Options include 'primary', 'secondary', 'error', 'info', 'success', 'warning', or a custom color.
valuenumber0The value of the progress indicator for the determinate variant (0-100).
variantstring'indeterminate'The variant to use. Options include 'determinate', 'indeterminate', 'buffer', or 'query'.
valueBuffernumber-The value for the buffer variant (used for the buffer bar).
sxobject-The system prop that allows defining custom styles.

Variants

  1. Determinate: Shows a fixed progress value (0-100). Perfect for when you know the exact progress percentage.
  2. Indeterminate: Continuously animates, suitable when progress isn't quantifiable.
  3. Buffer: Shows both progress and a buffer bar, useful for streaming operations.
  4. Query: Indicates a loading state before the progress starts.
import { LinearProgress } from '@mui/material';

// Determinate - For known progress
<LinearProgress variant="determinate" value={75} />

// Indeterminate - For unknown progress duration
<LinearProgress variant="indeterminate" />

// Buffer - For operations with buffering (like video streaming)
<LinearProgress variant="buffer" value={60} valueBuffer={80} />

// Query - For initial loading state
<LinearProgress variant="query" />

Circular Progress Component

The CircularProgress component displays a circular animation. It's useful for compact spaces or as a more subtle indicator.

Key Props

PropTypeDefaultDescription
colorstring'primary'The color of the component. Options include 'primary', 'secondary', 'error', 'info', 'success', 'warning', or a custom color.
sizenumber | string40The size of the circle (in pixels or with CSS unit).
thicknessnumber3.6The thickness of the circle (as a percentage of size).
valuenumber0The value of the progress indicator for the determinate variant (0-100).
variantstring'indeterminate'The variant to use. Options include 'determinate' or 'indeterminate'.
disableShrinkbooleanfalseIf true, the shrink animation won't be applied (indeterminate variant only).
import { CircularProgress } from '@mui/material';

// Determinate
<CircularProgress variant="determinate" value={75} />

// Indeterminate
<CircularProgress />

// Custom size and thickness
<CircularProgress size={60} thickness={4.5} />

Accessibility Considerations

Both progress components are designed with accessibility in mind. They include appropriate ARIA attributes by default:

  • role="progressbar" is automatically applied
  • aria-valuenow, aria-valuemin, and aria-valuemax are set for determinate variants
  • For indeterminate variants, aria-busy="true" is used

However, you should also consider adding descriptive text for screen readers:

<Box sx={{ position: 'relative', display: 'inline-flex' }}>
  <CircularProgress 
    variant="determinate" 
    value={75} 
    aria-label="File upload progress" 
  />
  <Box
    sx={{
      top: 0,
      left: 0,
      bottom: 0,
      right: 0,
      position: 'absolute',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
    }}
  >
    <Typography
      variant="caption"
      component="div"
      color="text.secondary"
    >{75}%</Typography>
  </Box>
</Box>

Step-by-Step Guide: Building a File Upload Progress System

Now that we understand the components, let's build a complete file upload system with progress tracking. We'll break this down into manageable steps.

Step 1: Setting Up the Project

First, let's create a new React project and install the necessary dependencies.

# Create a new React project
npx create-react-app file-upload-progress

# Navigate to the project directory
cd file-upload-progress

# Install required dependencies
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material axios

These packages provide:

  • MUI components and styling system
  • Icon components for visual indicators
  • Axios for handling HTTP requests with progress tracking

Step 2: Creating the File Upload Component Structure

Let's start by creating a basic file upload component with a dropzone area and progress indicator.

// src/components/FileUploader.jsx
import React, { useState } from 'react';
import {
  Box,
  Button,
  Typography,
  Paper,
  LinearProgress,
  Stack,
  Alert,
} from '@mui/material';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';

const FileUploader = () => {
  const [file, setFile] = useState(null);
  const [progress, setProgress] = useState(0);
  const [uploading, setUploading] = useState(false);
  const [uploadStatus, setUploadStatus] = useState(null); // 'success', 'error', or null
  
  const handleFileChange = (event) => {
    const selectedFile = event.target.files[0];
    if (selectedFile) {
      setFile(selectedFile);
      // Reset states when a new file is selected
      setProgress(0);
      setUploading(false);
      setUploadStatus(null);
    }
  };
  
  const handleUpload = () => {
    // We'll implement this in the next step
    console.log("Upload functionality will be implemented next");
  };
  
  return (
    <Paper 
      elevation={3} 
      sx={{ 
        p: 3, 
        maxWidth: 500, 
        mx: 'auto', 
        mt: 4,
        borderRadius: 2
      }}
    >
      <Typography variant="h5" gutterBottom align="center">
        File Upload with Progress
      </Typography>
      
      <Box 
        sx={{ 
          border: '2px dashed #ccc', 
          borderRadius: 2, 
          p: 3, 
          textAlign: 'center',
          mb: 3,
          bgcolor: 'background.paper',
          cursor: 'pointer',
          '&:hover': {
            borderColor: 'primary.main',
          }
        }}
        onClick={() => document.getElementById('file-input').click()}
      >
        <input
          id="file-input"
          type="file"
          style={{ display: 'none' }}
          onChange={handleFileChange}
        />
        <CloudUploadIcon sx={{ fontSize: 48, color: 'text.secondary', mb: 1 }} />
        <Typography variant="body1">
          {file ? file.name : "Drag and drop a file here or click to browse"}
        </Typography>
        {file && (
          <Typography variant="caption" display="block" color="text.secondary">
            {(file.size / 1024 / 1024).toFixed(2)} MB
          </Typography>
        )}
      </Box>
      
      {file && !uploading && !uploadStatus && (
        <Button 
          variant="contained" 
          fullWidth 
          onClick={handleUpload}
          sx={{ mb: 2 }}
        >
          Upload File
        </Button>
      )}
      
      {uploading && (
        <Box sx={{ width: '100%', mt: 2, mb: 2 }}>
          <LinearProgress 
            variant="determinate" 
            value={progress} 
            sx={{ height: 10, borderRadius: 5 }}
          />
          <Typography variant="body2" sx={{ mt: 1, textAlign: 'center' }}>
            Uploading: {progress}%
          </Typography>
        </Box>
      )}
      
      {uploadStatus === 'success' && (
        <Alert severity="success" sx={{ mt: 2 }}>
          File uploaded successfully!
        </Alert>
      )}
      
      {uploadStatus === 'error' && (
        <Alert severity="error" sx={{ mt: 2 }}>
          Error uploading file. Please try again.
        </Alert>
      )}
    </Paper>
  );
};

export default FileUploader;

This component sets up:

  • A styled dropzone area for file selection
  • States to track the selected file, upload progress, and status
  • A progress bar that will display during upload
  • Status alerts for success and error states

Step 3: Implementing File Upload with Progress Tracking

Now, let's implement the actual file upload functionality with progress tracking using Axios.

// src/components/FileUploader.jsx
// Add this import at the top
import axios from 'axios';

// Then update the handleUpload function
const handleUpload = async () => {
  if (!file) return;
  
  setUploading(true);
  setProgress(0);
  setUploadStatus(null);
  
  const formData = new FormData();
  formData.append('file', file);
  
  try {
    // Replace with your actual upload 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
        );
        setProgress(percentCompleted);
      },
    });
    
    setUploadStatus('success');
  } catch (error) {
    console.error('Upload error:', error);
    setUploadStatus('error');
  } finally {
    setUploading(false);
  }
};

This implementation:

  • Creates a FormData object to send the file
  • Uses Axios's onUploadProgress callback to track and update progress
  • Handles success and error states appropriately
  • Sets the uploading state to false when complete

Step 4: Creating a Multi-Step Upload Progress Indicator

Let's enhance our uploader with a multi-step progress indicator that shows different stages of the upload process.

// src/components/StepProgressUploader.jsx
import React, { useState } from 'react';
import {
  Box,
  Button,
  Typography,
  Paper,
  Stepper,
  Step,
  StepLabel,
  LinearProgress,
  CircularProgress,
  Alert,
  Stack,
} from '@mui/material';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import ErrorIcon from '@mui/icons-material/Error';
import axios from 'axios';

const steps = ['Select File', 'Prepare Upload', 'Uploading', 'Processing', 'Complete'];

const StepProgressUploader = () => {
  const [file, setFile] = useState(null);
  const [activeStep, setActiveStep] = useState(0);
  const [progress, setProgress] = useState(0);
  const [uploading, setUploading] = useState(false);
  const [uploadStatus, setUploadStatus] = useState(null); // 'success', 'error', or null
  
  const handleFileChange = (event) => {
    const selectedFile = event.target.files[0];
    if (selectedFile) {
      setFile(selectedFile);
      setActiveStep(1); // Move to "Prepare Upload" step
      setProgress(0);
      setUploading(false);
      setUploadStatus(null);
    }
  };
  
  const handleUpload = async () => {
    if (!file) return;
    
    setUploading(true);
    setActiveStep(2); // Move to "Uploading" step
    setProgress(0);
    
    const formData = new FormData();
    formData.append('file', file);
    
    try {
      // Simulate upload preparation (in real app, you might validate file or get upload URL)
      await new Promise(resolve => setTimeout(resolve, 1000));
      
      // Start the actual upload
      // Replace with your actual upload endpoint
      await axios.post('https://httpbin.org/post', formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
        },
        onUploadProgress: (progressEvent) => {
          const percentCompleted = Math.round(
            (progressEvent.loaded * 100) / progressEvent.total
          );
          setProgress(percentCompleted);
        },
      });
      
      // Move to processing step
      setActiveStep(3);
      
      // Simulate server-side processing
      await new Promise(resolve => setTimeout(resolve, 2000));
      
      // Complete
      setActiveStep(4);
      setUploadStatus('success');
    } catch (error) {
      console.error('Upload error:', error);
      setUploadStatus('error');
    } finally {
      setUploading(false);
    }
  };
  
  const resetUpload = () => {
    setFile(null);
    setActiveStep(0);
    setProgress(0);
    setUploading(false);
    setUploadStatus(null);
  };
  
  // Determine the step status (completed, in progress, or pending)
  const getStepStatus = (step) => {
    if (uploadStatus === 'error' && activeStep === step) {
      return 'error';
    }
    if (step < activeStep) {
      return 'completed';
    }
    if (step === activeStep) {
      return 'active';
    }
    return 'pending';
  };
  
  return (
    <Paper 
      elevation={3} 
      sx={{ 
        p: 3, 
        maxWidth: 600, 
        mx: 'auto', 
        mt: 4,
        borderRadius: 2
      }}
    >
      <Typography variant="h5" gutterBottom align="center">
        Multi-Step File Upload
      </Typography>
      
      <Stepper activeStep={activeStep} alternativeLabel sx={{ mb: 4 }}>
        {steps.map((label, index) => {
          const stepStatus = getStepStatus(index);
          
          return (
            <Step key={label} completed={stepStatus === 'completed'}>
              <StepLabel
                error={stepStatus === 'error'}
                StepIconProps={{
                  icon: index + 1,
                }}
              >
                {label}
              </StepLabel>
            </Step>
          );
        })}
      </Stepper>
      
      {/* Step 0: Select File */}
      {activeStep === 0 && (
        <Box 
          sx={{ 
            border: '2px dashed #ccc', 
            borderRadius: 2, 
            p: 3, 
            textAlign: 'center',
            mb: 3,
            bgcolor: 'background.paper',
            cursor: 'pointer',
            '&:hover': {
              borderColor: 'primary.main',
            }
          }}
          onClick={() => document.getElementById('file-input').click()}
        >
          <input
            id="file-input"
            type="file"
            style={{ display: 'none' }}
            onChange={handleFileChange}
          />
          <CloudUploadIcon sx={{ fontSize: 48, color: 'text.secondary', mb: 1 }} />
          <Typography variant="body1">
            Drag and drop a file here or click to browse
          </Typography>
        </Box>
      )}
      
      {/* Step 1: Prepare Upload */}
      {activeStep === 1 && (
        <Box sx={{ textAlign: 'center', mb: 3 }}>
          <Typography variant="body1" gutterBottom>
            Selected file: {file.name}
          </Typography>
          <Typography variant="body2" color="text.secondary" gutterBottom>
            Size: {(file.size / 1024 / 1024).toFixed(2)} MB
          </Typography>
          <Typography variant="body2" gutterBottom sx={{ mt: 2 }}>
            Click the button below to start the upload process.
          </Typography>
          <Button 
            variant="contained" 
            onClick={handleUpload}
            sx={{ mt: 2 }}
          >
            Start Upload
          </Button>
        </Box>
      )}
      
      {/* Step 2: Uploading */}
      {activeStep === 2 && (
        <Box sx={{ textAlign: 'center', mb: 3 }}>
          <Typography variant="body1" gutterBottom>
            Uploading {file.name}...
          </Typography>
          <LinearProgress 
            variant="determinate" 
            value={progress} 
            sx={{ height: 10, borderRadius: 5, my: 2 }}
          />
          <Typography variant="body2" color="text.secondary">
            {progress}% complete
          </Typography>
        </Box>
      )}
      
      {/* Step 3: Processing */}
      {activeStep === 3 && (
        <Box sx={{ textAlign: 'center', mb: 3 }}>
          <CircularProgress size={48} thickness={4} sx={{ mb: 2 }} />
          <Typography variant="body1">
            Processing your file...
          </Typography>
          <Typography variant="body2" color="text.secondary">
            This may take a moment
          </Typography>
        </Box>
      )}
      
      {/* Step 4: Complete */}
      {activeStep === 4 && (
        <Box sx={{ textAlign: 'center', mb: 3 }}>
          {uploadStatus === 'success' ? (
            <>
              <CheckCircleIcon 
                sx={{ fontSize: 48, color: 'success.main', mb: 2 }} 
              />
              <Typography variant="h6" gutterBottom>
                Upload Complete!
              </Typography>
              <Typography variant="body1" gutterBottom>
                Your file has been successfully uploaded and processed.
              </Typography>
            </>
          ) : uploadStatus === 'error' ? (
            <>
              <ErrorIcon 
                sx={{ fontSize: 48, color: 'error.main', mb: 2 }} 
              />
              <Typography variant="h6" gutterBottom>
                Upload Failed
              </Typography>
              <Typography variant="body1" gutterBottom>
                There was an error processing your file. Please try again.
              </Typography>
            </>
          ) : null}
          
          <Button 
            variant="outlined" 
            onClick={resetUpload}
            sx={{ mt: 2 }}
          >
            Upload Another File
          </Button>
        </Box>
      )}
      
      {uploadStatus === 'error' && activeStep !== 4 && (
        <Alert severity="error" sx={{ mt: 2 }}>
          Error during the upload process. Please try again.
          <Button 
            size="small" 
            onClick={resetUpload}
            sx={{ ml: 2 }}
          >
            Restart
          </Button>
        </Alert>
      )}
    </Paper>
  );
};

export default StepProgressUploader;

This enhanced component:

  • Uses a Stepper to show the different stages of the upload process
  • Provides visual feedback at each step
  • Simulates a complete upload flow including server-side processing
  • Handles errors at any stage
  • Allows restarting the process

Step 5: Integrating with Your App

Now, let's integrate our uploader into the main App component:

// src/App.js
import React from 'react';
import { 
  CssBaseline, 
  ThemeProvider, 
  createTheme,
  Container,
  Typography,
  Box,
  Tabs,
  Tab
} from '@mui/material';
import FileUploader from './components/FileUploader';
import StepProgressUploader from './components/StepProgressUploader';

// Create a custom theme
const theme = createTheme({
  palette: {
    primary: {
      main: '#2196f3',
    },
    secondary: {
      main: '#f50057',
    },
  },
  components: {
    MuiLinearProgress: {
      styleOverrides: {
        root: {
          borderRadius: 5,
        },
      },
    },
  },
});

function App() {
  const [tabValue, setTabValue] = React.useState(0);

  const handleTabChange = (event, newValue) => {
    setTabValue(newValue);
  };

  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <Container maxWidth="md">
        <Box sx={{ my: 4 }}>
          <Typography variant="h4" component="h1" gutterBottom align="center">
            React MUI File Upload Progress Demo
          </Typography>
          
          <Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
            <Tabs 
              value={tabValue} 
              onChange={handleTabChange} 
              centered
            >
              <Tab label="Simple Uploader" />
              <Tab label="Multi-Step Uploader" />
            </Tabs>
          </Box>
          
          {tabValue === 0 && <FileUploader />}
          {tabValue === 1 && <StepProgressUploader />}
        </Box>
      </Container>
    </ThemeProvider>
  );
}

export default App;

This App component:

  • Sets up a custom theme for consistent styling
  • Creates a tabbed interface to showcase both uploader variants
  • Provides a clean, responsive container for our components

Step 6: Creating a Custom Progress Component

Let's create a more advanced custom progress component that combines both linear and circular progress indicators for a richer UI:

// src/components/CustomProgressIndicator.jsx
import React from 'react';
import {
  Box,
  Typography,
  LinearProgress,
  CircularProgress,
} from '@mui/material';

const CustomProgressIndicator = ({ 
  progress, 
  size = 80, 
  thickness = 4,
  variant = 'determinate',
  showPercentage = true,
  label,
  sx = {}
}) => {
  return (
    <Box sx={{ position: 'relative', display: 'inline-flex', ...sx }}>
      <CircularProgress
        variant={variant}
        value={progress}
        size={size}
        thickness={thickness}
        aria-label={label || "Progress indicator"}
      />
      
      {variant === 'determinate' && showPercentage && (
        <Box
          sx={{
            top: 0,
            left: 0,
            bottom: 0,
            right: 0,
            position: 'absolute',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
          }}
        >
          <Typography
            variant="caption"
            component="div"
            color="text.secondary"
            sx={{ fontWeight: 'bold' }}
          >
            {progress}%
          </Typography>
        </Box>
      )}
      
      {label && (
        <Typography
          variant="body2"
          component="div"
          sx={{ mt: 1, textAlign: 'center' }}
        >
          {label}
        </Typography>
      )}
    </Box>
  );
};

// A component that combines both circular and linear progress
export const CombinedProgress = ({ 
  progress, 
  label,
  sx = {}
}) => {
  return (
    <Box sx={{ textAlign: 'center', ...sx }}>
      <CustomProgressIndicator 
        progress={progress} 
        label={label}
        sx={{ mb: 2 }}
      />
      
      <LinearProgress 
        variant="determinate" 
        value={progress} 
        sx={{ 
          height: 8, 
          borderRadius: 4,
          width: '100%',
          maxWidth: 300,
          mx: 'auto'
        }}
      />
    </Box>
  );
};

export default CustomProgressIndicator;

Now let's integrate this custom component into our StepProgressUploader:

// In StepProgressUploader.jsx, add this import
import { CombinedProgress } from './CustomProgressIndicator';

// Then replace the uploading step (Step 2) with:
{/* Step 2: Uploading */}
{activeStep === 2 && (
  <Box sx={{ textAlign: 'center', mb: 3 }}>
    <Typography variant="body1" gutterBottom>
      Uploading {file.name}...
    </Typography>
    <CombinedProgress 
      progress={progress}
      label="Upload in progress" 
      sx={{ my: 2 }}
    />
    <Typography variant="body2" color="text.secondary">
      Please don't close this window
    </Typography>
  </Box>
)}

This custom component:

  • Combines circular and linear progress for a more engaging UI
  • Provides configurable options for size, thickness, and labels
  • Maintains accessibility with appropriate ARIA attributes
  • Can be easily reused throughout your application

Step 7: Implementing a Chunked File Upload with Progress

For large files, chunked uploads are more reliable. Let's implement a chunked upload with progress tracking:

// src/components/ChunkedUploader.jsx
import React, { useState, useCallback } from 'react';
import {
  Box,
  Button,
  Typography,
  Paper,
  LinearProgress,
  Alert,
  Stack,
  Divider,
} from '@mui/material';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import axios from 'axios';

const ChunkedUploader = () => {
  const [file, setFile] = useState(null);
  const [chunkSize, setChunkSize] = useState(1024 * 1024); // 1MB chunks
  const [totalChunks, setTotalChunks] = useState(0);
  const [currentChunk, setCurrentChunk] = useState(0);
  const [progress, setProgress] = useState(0);
  const [uploading, setUploading] = useState(false);
  const [uploadStatus, setUploadStatus] = useState(null);
  const [uploadId, setUploadId] = useState('');
  
  const handleFileChange = (event) => {
    const selectedFile = event.target.files[0];
    if (selectedFile) {
      setFile(selectedFile);
      const chunks = Math.ceil(selectedFile.size / chunkSize);
      setTotalChunks(chunks);
      setCurrentChunk(0);
      setProgress(0);
      setUploading(false);
      setUploadStatus(null);
      setUploadId('');
    }
  };
  
  const uploadChunk = useCallback(async (chunk, chunkIndex, uploadId) => {
    const formData = new FormData();
    formData.append('file', chunk, file.name);
    formData.append('chunkIndex', chunkIndex);
    formData.append('totalChunks', totalChunks);
    
    if (uploadId) {
      formData.append('uploadId', uploadId);
    }
    
    // Replace with your actual chunked upload endpoint
    const response = await axios.post('https://httpbin.org/post', formData, {
      headers: {
        'Content-Type': 'multipart/form-data',
      },
    });
    
    // In a real implementation, your server would return an uploadId
    // for the first chunk, which you'd use for subsequent chunks
    return response.data;
  }, [file, totalChunks]);
  
  const handleUpload = async () => {
    if (!file) return;
    
    setUploading(true);
    setProgress(0);
    setCurrentChunk(0);
    
    try {
      let currentUploadId = uploadId;
      
      for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
        const start = chunkIndex * chunkSize;
        const end = Math.min(file.size, start + chunkSize);
        const chunk = file.slice(start, end);
        
        // Upload the chunk
        const response = await uploadChunk(chunk, chunkIndex, currentUploadId);
        
        // In a real implementation, get the uploadId from the first chunk response
        if (chunkIndex === 0 && !currentUploadId) {
          // This is a simulation - in reality, you'd get this from your server
          currentUploadId = 'sim-upload-' + Date.now();
          setUploadId(currentUploadId);
        }
        
        // Update progress
        setCurrentChunk(chunkIndex + 1);
        const newProgress = Math.round(((chunkIndex + 1) / totalChunks) * 100);
        setProgress(newProgress);
      }
      
      // All chunks uploaded successfully
      setUploadStatus('success');
    } catch (error) {
      console.error('Chunk upload error:', error);
      setUploadStatus('error');
    } finally {
      setUploading(false);
    }
  };
  
  return (
    <Paper 
      elevation={3} 
      sx={{ 
        p: 3, 
        maxWidth: 500, 
        mx: 'auto', 
        mt: 4,
        borderRadius: 2
      }}
    >
      <Typography variant="h5" gutterBottom align="center">
        Chunked File Upload
      </Typography>
      
      <Box 
        sx={{ 
          border: '2px dashed #ccc', 
          borderRadius: 2, 
          p: 3, 
          textAlign: 'center',
          mb: 3,
          bgcolor: 'background.paper',
          cursor: 'pointer',
          '&:hover': {
            borderColor: 'primary.main',
          }
        }}
        onClick={() => document.getElementById('chunked-file-input').click()}
      >
        <input
          id="chunked-file-input"
          type="file"
          style={{ display: 'none' }}
          onChange={handleFileChange}
        />
        <CloudUploadIcon sx={{ fontSize: 48, color: 'text.secondary', mb: 1 }} />
        <Typography variant="body1">
          {file ? file.name : "Select a large file for chunked upload"}
        </Typography>
        {file && (
          <Typography variant="caption" display="block" color="text.secondary">
            {(file.size / 1024 / 1024).toFixed(2)} MB • {totalChunks} chunks
          </Typography>
        )}
      </Box>
      
      {file && !uploading && !uploadStatus && (
        <Button 
          variant="contained" 
          fullWidth 
          onClick={handleUpload}
          sx={{ mb: 2 }}
        >
          Start Chunked Upload
        </Button>
      )}
      
      {uploading && (
        <Box sx={{ width: '100%', mt: 2, mb: 2 }}>
          <Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1 }}>
            <Typography variant="body2" sx={{ flexGrow: 1 }}>
              Overall Progress:
            </Typography>
            <Typography variant="body2" color="text.secondary">
              {progress}%
            </Typography>
          </Stack>
          
          <LinearProgress 
            variant="determinate" 
            value={progress} 
            sx={{ height: 10, borderRadius: 5, mb: 2 }}
          />
          
          <Divider sx={{ my: 2 }} />
          
          <Typography variant="body2" sx={{ mb: 1 }}>
            Uploading chunk {currentChunk} of {totalChunks}
          </Typography>
          
          <LinearProgress 
            variant="indeterminate"
            sx={{ height: 6, borderRadius: 5 }}
          />
        </Box>
      )}
      
      {uploadStatus === 'success' && (
        <Alert severity="success" sx={{ mt: 2 }}>
          All chunks uploaded successfully! Your file is complete.
        </Alert>
      )}
      
      {uploadStatus === 'error' && (
        <Alert severity="error" sx={{ mt: 2 }}>
          Error uploading chunks. Please try again.
        </Alert>
      )}
    </Paper>
  );
};

export default ChunkedUploader;

This chunked uploader:

  • Divides large files into manageable chunks (1MB by default)
  • Tracks both overall progress and individual chunk uploads
  • Simulates a complete chunked upload flow
  • Provides detailed progress information to the user

Step 8: Implementing Drag and Drop Functionality

Let's enhance our uploader with proper drag and drop support:

// src/components/DragDropUploader.jsx
import React, { useState, useCallback } from 'react';
import {
  Box,
  Button,
  Typography,
  Paper,
  LinearProgress,
  Alert,
  Stack,
} from '@mui/material';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import { CombinedProgress } from './CustomProgressIndicator';
import axios from 'axios';

const DragDropUploader = () => {
  const [file, setFile] = useState(null);
  const [progress, setProgress] = useState(0);
  const [uploading, setUploading] = useState(false);
  const [uploadStatus, setUploadStatus] = useState(null);
  const [dragActive, setDragActive] = useState(false);
  
  const handleDrag = useCallback((e) => {
    e.preventDefault();
    e.stopPropagation();
    
    if (e.type === 'dragenter' || e.type === 'dragover') {
      setDragActive(true);
    } else if (e.type === 'dragleave') {
      setDragActive(false);
    }
  }, []);
  
  const handleDrop = useCallback((e) => {
    e.preventDefault();
    e.stopPropagation();
    setDragActive(false);
    
    if (e.dataTransfer.files && e.dataTransfer.files[0]) {
      const droppedFile = e.dataTransfer.files[0];
      setFile(droppedFile);
      setProgress(0);
      setUploading(false);
      setUploadStatus(null);
    }
  }, []);
  
  const handleFileChange = (event) => {
    const selectedFile = event.target.files[0];
    if (selectedFile) {
      setFile(selectedFile);
      setProgress(0);
      setUploading(false);
      setUploadStatus(null);
    }
  };
  
  const handleUpload = async () => {
    if (!file) return;
    
    setUploading(true);
    setProgress(0);
    
    const formData = new FormData();
    formData.append('file', file);
    
    try {
      await axios.post('https://httpbin.org/post', formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
        },
        onUploadProgress: (progressEvent) => {
          const percentCompleted = Math.round(
            (progressEvent.loaded * 100) / progressEvent.total
          );
          setProgress(percentCompleted);
        },
      });
      
      setUploadStatus('success');
    } catch (error) {
      console.error('Upload error:', error);
      setUploadStatus('error');
    } finally {
      setUploading(false);
    }
  };
  
  return (
    <Paper 
      elevation={3} 
      sx={{ 
        p: 3, 
        maxWidth: 500, 
        mx: 'auto', 
        mt: 4,
        borderRadius: 2
      }}
    >
      <Typography variant="h5" gutterBottom align="center">
        Drag & Drop File Upload
      </Typography>
      
      <Box 
        sx={{ 
          border: '2px dashed',
          borderColor: dragActive ? 'primary.main' : '#ccc',
          borderRadius: 2, 
          p: 3, 
          textAlign: 'center',
          mb: 3,
          bgcolor: dragActive ? 'action.hover' : 'background.paper',
          cursor: 'pointer',
          transition: 'all 0.2s ease-in-out',
          '&:hover': {
            borderColor: 'primary.main',
          }
        }}
        onClick={() => document.getElementById('drag-drop-file-input').click()}
        onDragEnter={handleDrag}
        onDragLeave={handleDrag}
        onDragOver={handleDrag}
        onDrop={handleDrop}
      >
        <input
          id="drag-drop-file-input"
          type="file"
          style={{ display: 'none' }}
          onChange={handleFileChange}
        />
        <CloudUploadIcon sx={{ 
          fontSize: 48, 
          color: dragActive ? 'primary.main' : 'text.secondary', 
          mb: 1,
          transition: 'color 0.2s ease-in-out'
        }} />
        <Typography variant="body1">
          {file ? file.name : "Drag and drop a file here or click to browse"}
        </Typography>
        {file && (
          <Typography variant="caption" display="block" color="text.secondary">
            {(file.size / 1024 / 1024).toFixed(2)} MB
          </Typography>
        )}
      </Box>
      
      {file && !uploading && !uploadStatus && (
        <Button 
          variant="contained" 
          fullWidth 
          onClick={handleUpload}
          sx={{ mb: 2 }}
        >
          Upload File
        </Button>
      )}
      
      {uploading && (
        <Box sx={{ width: '100%', mt: 2, mb: 2 }}>
          <CombinedProgress 
            progress={progress}
            label="Uploading..." 
            sx={{ my: 2 }}
          />
        </Box>
      )}
      
      {uploadStatus === 'success' && (
        <Alert severity="success" sx={{ mt: 2 }}>
          File uploaded successfully!
        </Alert>
      )}
      
      {uploadStatus === 'error' && (
        <Alert severity="error" sx={{ mt: 2 }}>
          Error uploading file. Please try again.
        </Alert>
      )}
    </Paper>
  );
};

export default DragDropUploader;

This drag and drop uploader:

  • Provides visual feedback when files are dragged over the drop zone
  • Handles all the necessary drag and drop events
  • Uses smooth transitions for a polished user experience
  • Integrates with our custom progress indicator

Advanced Customization and Theming

MUI's progress components can be extensively customized using the theme system and the sx prop. Let's explore some advanced customization options.

Custom Progress Colors and Animations

// Custom theme with progress customizations
const theme = createTheme({
  components: {
    MuiLinearProgress: {
      styleOverrides: {
        root: {
          borderRadius: 10,
          backgroundColor: 'rgba(0, 0, 0, 0.05)',
        },
        bar: {
          borderRadius: 10,
          // Create a gradient effect
          backgroundImage: 'linear-gradient(to right, #4facfe 0%, #00f2fe 100%)',
        },
      },
    },
    MuiCircularProgress: {
      styleOverrides: {
        circle: {
          // Make the circle stroke rounded at the ends
          strokeLinecap: 'round',
        },
      },
    },
  },
});

Creating a Themed Progress Bar Component

// src/components/ThemedProgress.jsx
import React from 'react';
import { Box, LinearProgress, Typography, useTheme } from '@mui/material';

const progressColors = {
  low: '#f44336', // Red
  medium: '#ff9800', // Orange
  high: '#4caf50', // Green
};

const ThemedProgress = ({ value, label, variant = 'determinate', size = 'medium' }) => {
  const theme = useTheme();
  
  // Determine color based on progress value
  const getProgressColor = (value) => {
    if (value < 30) return progressColors.low;
    if (value < 70) return progressColors.medium;
    return progressColors.high;
  };
  
  // Determine height based on size
  const getHeight = (size) => {
    switch (size) {
      case 'small': return 4;
      case 'medium': return 8;
      case 'large': return 12;
      default: return 8;
    }
  };
  
  const progressColor = variant === 'determinate' ? getProgressColor(value) : theme.palette.primary.main;
  
  return (
    <Box sx={{ width: '100%' }}>
      {label && (
        <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
          <Typography variant="body2">{label}</Typography>
          {variant === 'determinate' && (
            <Typography variant="body2" color="text.secondary">{value}%</Typography>
          )}
        </Box>
      )}
      <LinearProgress
        variant={variant}
        value={value}
        sx={{
          height: getHeight(size),
          borderRadius: 5,
          backgroundColor: 'rgba(0, 0, 0, 0.08)',
          '& .MuiLinearProgress-bar': {
            borderRadius: 5,
            backgroundColor: progressColor,
          },
        }}
      />
    </Box>
  );
};

export default ThemedProgress;

This ThemedProgress component:

  • Changes color based on the progress value
  • Supports different sizes
  • Includes an optional label with percentage display
  • Uses custom styling via the sx prop

Best Practices and Common Issues

When implementing file uploads with progress indicators, there are several best practices to follow and common issues to avoid.

Best Practices

  1. Provide Clear Feedback

    • Always show the current progress percentage
    • Include file name and size information
    • Indicate when processing is happening on the server side
  2. Handle Edge Cases

    • Implement proper error handling for network issues
    • Allow users to retry failed uploads
    • Provide clear error messages when uploads fail
  3. Optimize for Large Files

    • Use chunked uploads for large files
    • Implement resumable uploads when possible
    • Consider implementing file compression before upload
  4. Accessibility Considerations

    • Ensure progress indicators have proper ARIA attributes
    • Provide text alternatives for visual indicators
    • Make sure keyboard users can interact with upload controls
  5. Performance Optimizations

    • Debounce progress updates for smoother animations
    • Avoid unnecessary re-renders during progress updates
    • Use web workers for heavy client-side processing

Common Issues and Solutions

Issue: Progress Jumps or Appears Inconsistent

Solution: Use a throttled or debounced update function to smooth out progress updates:

import { useCallback, useState } from 'react';
import { throttle } from 'lodash';

function useThrottledProgress() {
  const [progress, setProgress] = useState(0);
  
  // Throttle progress updates to max once per 100ms
  const updateProgress = useCallback(
    throttle((newProgress) => {
      setProgress(newProgress);
    }, 100),
    []
  );
  
  return [progress, updateProgress];
}

// Usage in component
const [progress, updateProgress] = useThrottledProgress();

// In axios request
onUploadProgress: (progressEvent) => {
  const percentCompleted = Math.round(
    (progressEvent.loaded * 100) / progressEvent.total
  );
  updateProgress(percentCompleted);
}

Issue: No Progress Events in Some Browsers or Network Conditions

Solution: Implement a fallback indeterminate progress:

const [progress, setProgress] = useState(0);
const [progressAvailable, setProgressAvailable] = useState(true);
const progressTimeout = useRef(null);

// Set a timeout - if no progress events after 3 seconds, switch to indeterminate
useEffect(() => {
  progressTimeout.current = setTimeout(() => {
    setProgressAvailable(false);
  }, 3000);
  
  return () => {
    if (progressTimeout.current) {
      clearTimeout(progressTimeout.current);
    }
  };
}, []);

// In your render function
{uploading && (
  <LinearProgress 
    variant={progressAvailable ? "determinate" : "indeterminate"} 
    value={progress} 
  />
)}

Issue: Memory Leaks from Progress Updates after Component Unmount

Solution: Use a cleanup function with useEffect:

const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const isMounted = useRef(true);

// Set up cleanup when component unmounts
useEffect(() => {
  return () => {
    isMounted.current = false;
  };
}, []);

const handleUpload = async () => {
  setUploading(true);
  
  try {
    await axios.post('https://your-api.com/upload', formData, {
      onUploadProgress: (progressEvent) => {
        if (isMounted.current) {
          const percentCompleted = Math.round(
            (progressEvent.loaded * 100) / progressEvent.total
          );
          setProgress(percentCompleted);
        }
      },
    });
  } catch (error) {
    console.error(error);
  } finally {
    if (isMounted.current) {
      setUploading(false);
    }
  }
};

Issue: Progress Resets on Network Fluctuations

Solution: Implement a progress that never decreases during a single upload:

const [progress, setProgress] = useState(0);

const updateProgress = (newProgress) => {
  // Only update if the new progress is higher than current
  setProgress(prev => Math.max(prev, newProgress));
};

// In axios request
onUploadProgress: (progressEvent) => {
  const percentCompleted = Math.round(
    (progressEvent.loaded * 100) / progressEvent.total
  );
  updateProgress(percentCompleted);
}

Wrapping Up

In this comprehensive guide, we've explored how to use React MUI's Progress components to build a robust and user-friendly file upload system. We've covered everything from basic progress indicators to advanced multi-step uploaders with chunked file handling.

By leveraging MUI's flexible components and styling system, we've created visually appealing and accessible progress indicators that provide clear feedback to users during the upload process. We've also addressed common challenges and best practices to ensure your implementation is reliable and performant.

The techniques demonstrated here can be adapted for various use cases beyond file uploads, such as form submissions, data processing, or any long-running operation where visual feedback improves the user experience.