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();
}
});
Discover how at OpenReplay.com.
Understanding the Progress Event
The xhr.upload.onprogress event provides three crucial properties:
event.loaded: Bytes already uploadedevent.total: Total file size in bytesevent.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.