Back

How to Build an Upload Progress Bar with JavaScript

How to Build an Upload Progress Bar with JavaScript

Uploading files without visual feedback leaves users wondering if anything’s happening. A progress bar transforms this uncertainty into a clear, accessible experience that shows exactly how much of the upload is complete.

This article demonstrates how to build a real-time upload progress bar using JavaScript’s XMLHttpRequest API, semantic HTML elements, and accessibility best practices—creating a solution that works reliably across all modern browsers.

Key Takeaways

  • XMLHttpRequest remains the standard API for tracking upload progress, as Fetch API doesn’t support upload progress events
  • Semantic HTML with ARIA attributes ensures accessibility for all users
  • The solution works across all modern browsers without external dependencies
  • Proper error handling and user controls create a robust upload experience

Setting Up the HTML Structure

Start with semantic HTML that provides both visual and accessible feedback:

<form id="uploadForm">
  <label for="fileInput">Select file to upload:</label>
  <input type="file" id="fileInput" name="file" accept="image/*,application/pdf">
  
  <progress id="uploadProgress" value="0" max="100" aria-label="Upload progress"></progress>
  <span id="progressText" aria-live="polite">0% uploaded</span>
  
  <button type="submit">Upload File</button>
  <button type="button" id="cancelBtn" disabled>Cancel Upload</button>
</form>

The <progress> element provides native semantics that assistive technologies understand. The accompanying text percentage ensures users aren’t relying solely on visual cues. The aria-live="polite" attribute announces percentage changes to screen readers without interrupting other content.

Why XMLHttpRequest for Upload Progress

While the Fetch API handles most modern HTTP requests elegantly, it still doesn’t expose upload progress events. The XMLHttpRequest object remains the correct tool for tracking upload progress implementations because it provides the xhr.upload.onprogress event handler.

Note that only synchronous XMLHttpRequest is deprecated—asynchronous XHR remains a baseline, well-supported API that’s perfect for this use case.

Implementing the Upload Progress Bar with JavaScript

Here’s the complete implementation that handles file selection, upload tracking, and user controls:

const form = document.getElementById('uploadForm');
const fileInput = document.getElementById('fileInput');
const progressBar = document.getElementById('uploadProgress');
const progressText = document.getElementById('progressText');
const cancelBtn = document.getElementById('cancelBtn');

let currentXHR = null;

form.addEventListener('submit', (e) => {
  e.preventDefault();
  
  const file = fileInput.files[0];
  if (!file) return;
  
  // Validate file size (10MB limit example)
  const maxSize = 10 * 1024 * 1024;
  if (file.size > maxSize) {
    alert('File size exceeds 10MB limit');
    return;
  }
  
  uploadFile(file);
});

function uploadFile(file) {
  const formData = new FormData();
  formData.append('file', file);
  
  currentXHR = new XMLHttpRequest();
  
  // Track upload progress
  currentXHR.upload.onprogress = (event) => {
    if (event.lengthComputable) {
      const percentComplete = Math.round((event.loaded / event.total) * 100);
      updateProgress(percentComplete);
    } else {
      // Handle indeterminate progress
      progressBar.removeAttribute('value');
      progressText.textContent = 'Uploading...';
    }
  };
  
  // Handle completion
  currentXHR.onload = function() {
    if (currentXHR.status === 200) {
      updateProgress(100);
      progressText.textContent = 'Upload complete!';
      resetForm();
    } else {
      handleError('Upload failed: ' + currentXHR.statusText);
    }
  };
  
  // Handle errors
  currentXHR.onerror = () => handleError('Network error occurred');
  currentXHR.onabort = () => handleError('Upload cancelled');
  
  // Send request
  currentXHR.open('POST', '/api/upload', true);
  currentXHR.send(formData);
  
  // Enable cancel button
  cancelBtn.disabled = false;
}

function updateProgress(percent) {
  progressBar.value = percent;
  progressText.textContent = `${percent}% uploaded`;
}

function handleError(message) {
  progressText.textContent = message;
  progressBar.value = 0;
  resetForm();
}

function resetForm() {
  cancelBtn.disabled = true;
  currentXHR = null;
  setTimeout(() => {
    progressBar.value = 0;
    progressText.textContent = '0% uploaded';
  }, 2000);
}

// Cancel upload functionality
cancelBtn.addEventListener('click', () => {
  if (currentXHR) {
    currentXHR.abort();
  }
});

Understanding the Progress Event

The xhr.upload.onprogress event provides three crucial properties:

  • event.loaded: Bytes already uploaded
  • event.total: Total file size in bytes
  • event.lengthComputable: Boolean indicating if total size is known

When lengthComputable is true, calculate the percentage as (loaded / total) * 100. When false, the server hasn’t provided Content-Length headers, so show an indeterminate progress state by removing the progress element’s value attribute.

Styling for Better User Experience

Add CSS to make the upload progress implementation visually clear:

progress {
  width: 100%;
  height: 24px;
  margin: 10px 0;
}

/* Webkit browsers */
progress::-webkit-progress-bar {
  background-color: #f0f0f0;
  border-radius: 4px;
}

progress::-webkit-progress-value {
  background-color: #4CAF50;
  border-radius: 4px;
  transition: width 0.3s ease;
}

/* Firefox */
progress::-moz-progress-bar {
  background-color: #4CAF50;
  border-radius: 4px;
}

#progressText {
  display: block;
  margin-top: 5px;
  font-weight: 600;
}

Server-Side Considerations

The server endpoint should accept multipart/form-data uploads. Here’s a minimal Node.js example using Express and Multer:

const multer = require('multer');
const upload = multer({ dest: 'uploads/' });

app.post('/api/upload', upload.single('file'), (req, res) => {
  // File is available as req.file
  res.json({ success: true, filename: req.file.filename });
});

For production, add file type validation, virus scanning, and proper storage handling (local filesystem or cloud services like AWS S3).

Browser Compatibility and Progressive Enhancement

This approach works in all modern browsers because XMLHttpRequest Level 2 (which includes upload progress) has been supported since:

  • Chrome 7+
  • Firefox 3.5+
  • Safari 5+
  • Edge (all versions)

For older browsers, the upload still works—users just won’t see progress updates. The form degrades gracefully to a standard file upload.

Conclusion

Building an accessible upload progress bar requires XMLHttpRequest for progress events, semantic HTML for structure, and thoughtful JavaScript for handling various upload states. This implementation provides real-time feedback that works for all users, including those using assistive technologies, while maintaining broad browser compatibility without requiring any external libraries.

FAQs

Fetch API doesn't support upload progress events. XMLHttpRequest provides the xhr.upload.onprogress event handler specifically for tracking upload progress, making it the only viable option for real-time progress bars.

Create separate XMLHttpRequest instances for each file or queue them sequentially. Track individual progress and calculate overall progress by averaging percentages or summing loaded bytes across all files.

Without Content-Length headers, event.lengthComputable returns false. Show an indeterminate progress state by removing the progress element's value attribute and displaying generic loading text instead of percentages.

Standard XMLHttpRequest doesn't support resumable uploads. For this functionality, implement chunked uploads with server-side support or use specialized libraries that handle file splitting and resumption logic.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay