How to Build a Custom File Uploader with HTML5, JavaScript & Bootstrap
File uploading is a common feature in many web applications, whether it‘s for uploading profile pictures, documents, images, or other files. While you can use a basic HTML file input for selecting files, it offers a very poor user experience out of the box. The file input is not customizable and you can‘t easily show a preview of the selected files.
In this tutorial, we‘ll learn how to create a custom file uploader with a great user experience by combining HTML5, JavaScript, and Bootstrap. Here‘s what we‘ll cover:
- HTML5 File Handling Basics
- Creating the Uploader UI with Bootstrap
- Previewing Selected Files
- Validating File Sizes and Types
- Handling Multiple Files
- Showing Upload Progress
- Uploading Files via AJAX
- Accessibility & Best Practices
By the end of this article, you‘ll be able to implement a fully-functional and professional looking file uploader on your website. Let‘s get started!
HTML5 File Handling Basics
First, let‘s review the key HTML5 features we‘ll be using to build our file uploader.
The File Input
The humble <input type="file">
is the foundation of any file uploader. When clicked, it opens the user‘s file browser where they can select a file (or multiple files) from their device.
However, the default file input leaves a lot to be desired in terms of styling and UX. It can‘t be fully customized with CSS, and doesn‘t provide any way to preview the selected files.
The File API
The HTML5 File API allows us to work with files selected by the user through a file input. We can access information about the selected files, such as the name, size, and mime type.
Here‘s how to access the selected files from a file input in JavaScript:
const fileInput = document.querySelector(‘input[type="file"]‘);
fileInput.addEventListener(‘change‘, (event) => {
const files = event.target.files;
console.log(files); // FileList object
});
The File API also provides a way to read the contents of a file through the FileReader
interface, which we‘ll use later for previewing images.
Now that we know how to select and access files, let‘s start building the UI for our custom uploader!
Creating the Uploader UI with Bootstrap
To make our file uploader look professional and modern, we‘ll use the popular Bootstrap framework. This will also make our uploader responsive and mobile-friendly with minimal effort.
Here‘s the HTML skeleton of our uploader:
<div class="container">
<div class="row">
<div class="col">
<div class="custom-file-picker">
<input type="file" id="uploader" class="custom-file-picker__input">
<label for="uploader" class="custom-file-picker__label">
<i class="fa fa-upload custom-file-picker__icon"></i>
<span>Choose a file...</span>
</label>
<div class="custom-file-picker__preview"></div>
</div>
</div>
</div>
</div>
We‘re using a few Bootstrap classes to create a container, row, and column. Inside that, we have our custom file picker <div>
with three main parts:
- The
<input>
for selecting files (hidden with CSS) - The
<label>
that acts as our custom file picker button - An empty
<div>
where we‘ll display the previews later
To make it look nice, we‘ll add some CSS:
.custom-file-picker {
position: relative;
}
.custom-file-picker__label {
display: block;
padding: 1em 2em;
color: white;
background: #3498db;
border-radius: .4em;
cursor: pointer;
text-align: center;
}
.custom-file-picker__input {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
font-size: 1;
width:0;
height: 100%;
opacity: 0;
}
.custom-file-picker__icon {
margin-right: .5em;
vertical-align: middle;
}
.custom-file-picker__preview {
margin-top: 1em;
}
This hides the real file input visually, while making the label look like a clickable button. The <i>
tag is using a Font Awesome icon.
Previewing Selected Files
Next, let‘s add the ability to preview selected images before uploading them. We‘ll use the FileReader
API to read the contents of selected files and display them in our preview <div>
element.
function previewFiles(files) {
const preview = document.querySelector(‘.custom-file-picker__preview‘);
preview.innerHTML = ‘‘;
Array.from(files).forEach(file => {
const reader = new FileReader();
reader.addEventListener(‘load‘, (event) => {
const img = document.createElement(‘img‘);
img.setAttribute(‘src‘, event.target.result);
img.classList.add(‘custom-file-picker__preview-image‘);
preview.appendChild(img);
});
reader.readAsDataURL(file);
});
}
In this previewFiles
function, we first clear out any existing previews. Then we loop through each selected file, create a new FileReader
, and listen for when it‘s done loading the file.
Once loaded, we create a new <img>
element, set its src
to the loaded file data, and append it to the preview <div>
. The readAsDataURL
method is what actually loads the file and gives us a data URL we can use as an image src.
To use this function, we‘ll listen for the change
event on our file input and pass the selected files:
const fileInput = document.querySelector(‘.custom-file-picker__input‘);
fileInput.addEventListener(‘change‘, (event) => {
const files = event.target.files;
previewFiles(files);
});
Now when a user selects files, they‘ll instantly see image previews appear. For non-image files, you could show an icon indicating the file type.
Validating File Sizes and Types
Before uploading files to a server, it‘s a good idea to validate them client-side first. We can check if the selected files are of an allowed type and under a certain size limit.
Here‘s a validateFiles
function that takes an array of files and some options:
function validateFiles(files, options) {
const allowedTypes = options.allowedTypes || [];
const maxSize = options.maxSize || Infinity;
return Array.from(files).every(file => {
if (allowedTypes.length && !allowedTypes.includes(file.type)) {
alert(`File type not allowed: ${file.name}. Accepted: ${allowedTypes}`);
return false;
}
if (file.size > maxSize) {
alert(`File too large: ${file.name}. Max size: ${maxSize}`);
return false;
}
return true;
});
}
This checks each file against the allowed types array (if provided) and the max size limit. If any file fails validation, it alerts the user and returns false
, otherwise it returns true
.
To use it, we can add it to our change event listener:
fileInput.addEventListener(‘change‘, (event) => {
const files = event.target.files;
const options = {
allowedTypes: [‘image/jpeg‘, ‘image/png‘, ‘image/gif‘],
maxSize: 25 * 1024 * 1024, // 25MB
};
if (validateFiles(files, options)) {
previewFiles(files);
}
});
Now if a user selects files that don‘t meet our criteria, they‘ll see an alert message instead of the previews.
Handling Multiple Files
To allow selecting multiple files at once, simply add the multiple
attribute to the file input:
<input type="file" id="uploader" multiple>
Now the user can select multiple files by holding down Ctrl/Cmd or shift when clicking. The files
property on the input will be a FileList
containing all the selected files.
Our previewFiles
and validateFiles
functions already support handling multiple files, so no changes needed there. However, you‘ll probably want to put a limit on the number of files that can be selected at a time:
function validateFiles(files, options) {
const maxFiles = options.maxFiles || Infinity;
if (files.length > maxFiles) {
alert(`Too many files selected. Max allowed: ${maxFiles}`);
return false;
}
// ...rest of validation code
}
Showing Upload Progress
To show progress during file uploading, we can listen to the progress
event on the AJAX request. Here‘s an example using XMLHttpRequest
:
function uploadFiles(files) {
const formData = new FormData();
Array.from(files).forEach(file => {
formData.append(‘files[]‘, file);
});
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener(‘progress‘, (event) => {
const percent = event.loaded / event.total * 100;
console.log(`Upload progress: ${percent}%`);
// TODO: update progress bar
});
xhr.addEventListener(‘load‘, () => {
console.log(‘Upload complete!‘);
});
xhr.open(‘POST‘, ‘/upload‘);
xhr.send(formData);
}
Here we create a FormData
object and append each file to it. This is a convenient way to send files via AJAX. Then we create an XMLHttpRequest
and listen for the progress
event on its upload
property.
The progress
event gives us loaded
and total
properties which we can use to calculate the completion percentage. You can then update a visual progress bar in the UI (not shown here).
When the request loads, the upload is complete. You‘d want to check the response status and display a success or failure message to the user.
Uploading Files via AJAX
In the previous example, we sent the files to the server via XMLHttpRequest
. Another popular way to make AJAX requests is using the fetch
API, which returns a Promise.
Here‘s how to upload files using fetch
:
async function uploadFiles(files) {
const formData = new FormData();
Array.from(files).forEach(file => {
formData.append(‘files[]‘, file);
});
try {
const response = await fetch(‘/upload‘, {
method: ‘POST‘,
body: formData,
});
if (!response.ok) {
throw new Error(response.statusText);
}
console.log(‘Upload complete!‘);
} catch (error) {
console.error(‘Upload failed:‘, error);
}
}
We use the same FormData
approach, but pass it as the body
of the fetch
request. The async/await
syntax allows us to wait for the response and handle any errors with a try/catch
block.
Updating a progress bar with fetch
is a bit trickier than with XMLHttpRequest
. At the time of this writing, the fetch API does not provide a way to listen for progress events. However, you can use a library like Axios which adds support for progress via interceptors.
Accessibility & Best Practices
To wrap up, let‘s review some accessibility tips and best practices to keep in mind when building a file uploader:
-
Use a
<label>
– This allows clicking/tapping the label to activate the file input, making it easier to use for mouse and touchscreen users. It also helps screen reader users understand the purpose of the input. -
Set
accept
attribute – Use theaccept
attribute on the file input to specify allowed file types. This provides a hint to the browser about what files the user should select. For example:<input type="file" accept="image/*">
. -
Provide instructions – Clearly explain what types and sizes of files are allowed, and what the maximum number of files is (if applicable). You may also want to provide additional help text for screen reader users.
-
Handle missing support – Check if the browser supports the File API and related features, and provide fallback behavior or polyfills if necessary. Be sure to test in a variety of browsers!
-
Optimize performance – Avoid reading the contents of very large files in the browser, as it can slow things down or even crash the page. Consider uploading large files in chunks or using a server-side solution.
-
Validate server-side – While client-side validation improves the UX, it‘s not a substitute for server-side checks. Always verify and sanitize uploaded files on the backend before doing anything with them.
Conclusion
In this article, we learned how to build a fully-featured custom file uploader with HTML5, JavaScript, and Bootstrap. We covered how to:
- Create an accessible and responsive UI
- Preview selected files
- Validate file sizes and types
- Show upload progress
- Upload multiple files via AJAX
With these techniques, you‘ll be able to implement file uploading in your web apps with a much-improved user experience. Feel free to use this code as a starting point and adapt it to fit your needs.
The complete source code for this tutorial is available on GitHub. If you have any questions or feedback, let me know in the comments!
Further Reading: