Uploading files is a fundamental feature in many web applications, but the default file input element provided by browsers often lacks the aesthetics and functionality users expect. In this DevTip, we'll learn how to build a custom file uploader using JavaScript and HTML, creating a more engaging and user-friendly experience.

An illustration depicting file uploading.

Understanding file uploaders

A file uploader is a user interface component that allows users to select and upload files to a server. While the default HTML file input works, it doesn't offer much in terms of customization or user experience. Building a custom file uploader enables you to:

  • Provide a consistent look and feel with your application's design.
  • Improve usability with enhanced features like drag-and-drop support.
  • Offer better feedback to users with progress indicators and error messages.

Setting up the HTML structure

Let's start by creating the basic HTML structure for our custom file uploader. We'll use a <div> to represent the uploader area and an <input> of type file for file selection.

<!-- index.html -->
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Custom File Uploader</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div class="file-uploader">
      <p>Drag and drop files here or click to upload</p>
      <input type="file" id="file-input" multiple />
    </div>

    <script src="script.js"></script>
  </body>
</html>

In this structure:

  • The .file-uploader div will serve as the drop zone and clickable area.
  • The input element is hidden but will be triggered when the user clicks the uploader area.

Styling the file uploader with CSS

Next, we'll style the uploader to make it visually appealing and responsive to user interactions.

/* styles.css */
.file-uploader {
  border: 2px dashed #ccc;
  padding: 20px;
  text-align: center;
  cursor: pointer;
  position: relative;
}

.file-uploader.dragover {
  background-color: #f0f0f0;
}

.file-uploader input[type='file'] {
  display: none;
}

In this CSS:

  • We use a dashed border to indicate the drop zone area.
  • The cursor: pointer property makes it clear that the area is interactive.
  • We hide the file input using display: none.
  • The .dragover class will be used to change the appearance when a file is dragged over the uploader.

Implementing JavaScript to handle file selection and upload

Now, let's add JavaScript to handle file selection, drag-and-drop functionality, and uploading files to the server.

// script.js
const fileUploader = document.querySelector('.file-uploader')
const fileInput = document.getElementById('file-input')

fileUploader.addEventListener('click', () => {
  fileInput.click()
})

fileInput.addEventListener('change', (event) => {
  const files = event.target.files
  uploadFiles(files)
})

fileUploader.addEventListener('dragover', (event) => {
  event.preventDefault()
  fileUploader.classList.add('dragover')
})

fileUploader.addEventListener('dragleave', () => {
  fileUploader.classList.remove('dragover')
})

fileUploader.addEventListener('drop', (event) => {
  event.preventDefault()
  fileUploader.classList.remove('dragover')
  const files = event.dataTransfer.files
  uploadFiles(files)
})

function uploadFiles(files) {
  for (const file of files) {
    uploadFile(file)
  }
}

function uploadFile(file) {
  const url = '/upload' // Your server-side upload handler
  const formData = new FormData()
  formData.append('file', file)

  fetch(url, {
    method: 'POST',
    body: formData,
  })
    .then((response) => {
      console.log(`${file.name} uploaded successfully.`)
    })
    .catch((error) => {
      console.error(`Error uploading ${file.name}:`, error)
    })
}

Here, we:

  • Add an event listener to handle clicks on the uploader, triggering the hidden file input.
  • Handle the change event on the file input to get selected files.
  • Implement drag-and-drop functionality using dragover, dragleave, and drop events.
  • Define an uploadFiles function to manage multiple files.
  • Use the Fetch API to upload files to the server.

Enhancing user experience with progress indicators

To provide feedback to users during the upload process, we'll add progress indicators.

First, update the HTML to include a container for the progress bars:

<div class="upload-progress"></div>

Place this container where you want the progress bars to appear, typically below the uploader.

Next, modify the JavaScript uploadFile function to include progress tracking:

function uploadFile(file) {
  const url = '/upload'
  const formData = new FormData()
  formData.append('file', file)

  const xhr = new XMLHttpRequest()
  xhr.open('POST', url)

  // Create a progress bar
  const progressBar = document.createElement('div')
  progressBar.classList.add('progress-bar')
  progressBar.textContent = `Uploading ${file.name}`
  const uploadProgress = document.querySelector('.upload-progress')
  uploadProgress.appendChild(progressBar)

  xhr.upload.addEventListener('progress', (event) => {
    if (event.lengthComputable) {
      const percentComplete = ((event.loaded / event.total) * 100).toFixed(2)
      progressBar.style.width = `${percentComplete}%`
      progressBar.textContent = `${file.name} - ${percentComplete}%`
    }
  })

  xhr.addEventListener('load', () => {
    progressBar.classList.add('upload-complete')
    progressBar.textContent = `${file.name} uploaded successfully.`
  })

  xhr.addEventListener('error', () => {
    progressBar.classList.add('upload-error')
    progressBar.textContent = `Error uploading ${file.name}`
  })

  xhr.send(formData)
}

And add CSS for the progress bars:

/* styles.css */
.upload-progress {
  margin-top: 20px;
}

.progress-bar {
  background-color: #007bff;
  height: 20px;
  margin-bottom: 5px;
  color: #fff;
  line-height: 20px;
  padding: 0 10px;
  width: 0%;
  transition: width 0.3s;
}

.progress-bar.upload-complete {
  background-color: #28a745;
}

.progress-bar.upload-error {
  background-color: #dc3545;
}

Now, users will see a progress bar for each file being uploaded, providing real-time feedback.

Handling errors and validation

It's important to handle errors and validate files before uploading. Let's add file type and size validation.

Update the uploadFiles function:

function uploadFiles(files) {
  for (const file of files) {
    if (validateFile(file)) {
      uploadFile(file)
    }
  }
}

function validateFile(file) {
  const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf']
  const maxSize = 5 * 1024 * 1024 // 5MB

  if (!allowedTypes.includes(file.type)) {
    alert(`${file.name} is not an allowed file type.`)
    return false
  }

  if (file.size > maxSize) {
    alert(`${file.name} exceeds the maximum file size of 5MB.`)
    return false
  }

  return true
}

Now, the uploader will:

  • Allow only JPEG, PNG images, and PDF files.
  • Reject files larger than 5MB.
  • Alert the user if a file doesn't meet the criteria.

Testing the file uploader

To ensure our file uploader works across different scenarios:

  • Test with various file types and sizes.
  • Check the drag-and-drop functionality in different browsers.
  • Simulate network slowdowns to observe the progress indicators.
  • Handle server-side errors by modifying the server response to return error codes.

Conclusion

By building a custom file uploader, we've enhanced the user experience and added functionality beyond what the default file input provides. This approach allows for greater flexibility and a more professional appearance in your web applications.

For a more advanced file uploading solution with features like resumable uploads and cloud storage integration, consider using Uppy, an open-source file uploader from the folks at Transloadit.