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:

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:

  1. The <input> for selecting files (hidden with CSS)
  2. The <label> that acts as our custom file picker button
  3. 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 the accept 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:

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *