Last updated: February 5, 2025

<span aria-hidden="true" id="building-a-custom-file-uploader-with-javascript-and-html"></span>

# Building a custom file uploader with JavaScript and HTML

![Kevin van Zonneveld](/assets/images/teammates/avatar-kvz-4.jpg?dpl=dpl_5eQDcFwLGbzg3PfkSEW2a2j2x5pY)

**Kevin van Zonneveld**

Co-founder · Amsterdam, The Netherlands · Show bio

[](https://x.com/kvz)[](https://github.com/kvz)

Uploading files is a critical feature in many web applications. However, the native file input element offered by browsers often lacks the aesthetics and enhanced functionality modern users expect. In this DevTip, we demonstrate how to build a custom file uploader using JavaScript and HTML to create a sleek, robust, and user-friendly experience.

<span aria-hidden="true" id="understanding-file-uploaders"></span>

## Understanding file uploaders

A file uploader is a component that enables users to select and upload files to a server. While the default HTML file input is functional, it offers little in customization or rich user experience. Building a custom file uploader allows you to:

* Provide a consistent look and feel that aligns with your application design.
* Enhance usability with features like drag-and-drop support.
* Offer real-time feedback with progress indicators and error messages.

<span aria-hidden="true" id="setting-up-the-html-structure"></span>

## Setting up the HTML structure

Begin by creating the basic HTML structure for your custom file uploader. The following example demonstrates a clickable and drag-and-drop zone for file selection:

```html
<!-- 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 class="upload-progress"></div>
    </div>

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

```

In this setup:

* The `.file-uploader` div serves as both the drop zone and clickable area.
* The hidden `input` element facilitates file selection.
* The `.upload-progress` div displays upload progress for each file.

<span aria-hidden="true" id="styling-the-file-uploader-with-css"></span>

## Styling the file uploader with CSS

Enhance the visual appeal and interactive feedback with the following CSS:

```css
/* styles.css */
.file-uploader {
  border: 2px dashed #ccc;
  padding: 20px;
  text-align: center;
  cursor: pointer;
  position: relative;
  border-radius: 8px;
  transition: background-color 0.3s ease;
}

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

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

.upload-progress {
  margin-top: 20px;
}

.progress-bar {
  background-color: #007bff;
  height: 24px;
  margin-bottom: 8px;
  color: #fff;
  line-height: 24px;
  padding: 0 12px;
  width: 0%;
  transition: width 0.3s ease;
  border-radius: 4px;
  font-size: 14px;
}

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

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

```

<span aria-hidden="true" id="implementing-javascript-to-handle-file-selection-and-upload"></span>

## Implementing JavaScript to handle file selection and upload

The JavaScript implementation below uses the Fetch API for a modern, robust approach to file uploads, including chunked upload support, comprehensive error handling, and CSRF protection.

```javascript
class FileUploader {
  constructor() {
    this.fileUploader = document.querySelector('.file-uploader')
    this.fileInput = document.getElementById('file-input')
    this.progressContainer = document.querySelector('.upload-progress')
    this.maxFileSize = 10 * 1024 * 1024 // 10MB
    this.allowedTypes = ['image/jpeg', 'image/png', 'application/pdf']
    this.csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
    this.retryAttempts = 3
    this.chunkSize = 1024 * 1024 // 1MB chunks

    this.initializeEventListeners()
  }

  initializeEventListeners() {
    this.fileUploader.addEventListener('click', () => this.fileInput.click())
    this.fileInput.addEventListener('change', (event) => this.handleFiles(event.target.files))
    this.fileUploader.addEventListener('dragover', this.handleDragOver.bind(this))
    this.fileUploader.addEventListener('dragleave', this.handleDragLeave.bind(this))
    this.fileUploader.addEventListener('drop', this.handleDrop.bind(this))
  }

  handleDragOver(event) {
    event.preventDefault()
    this.fileUploader.classList.add('dragover')
  }

  handleDragLeave() {
    this.fileUploader.classList.remove('dragover')
  }

  handleDrop(event) {
    event.preventDefault()
    this.fileUploader.classList.remove('dragover')
    this.handleFiles(event.dataTransfer.files)
  }

  async handleFiles(files) {
    for (const file of files) {
      if (this.validateFile(file)) {
        await this.uploadFileWithChunks(file)
      }
    }
  }

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

    if (file.size > this.maxFileSize) {
      this.showError(`${file.name} exceeds the maximum file size of 10MB.`)
      return false
    }

    return true
  }

  async uploadFileWithChunks(file) {
    const progressBar = this.createProgressBar(file.name)
    const chunks = Math.ceil(file.size / this.chunkSize)
    let uploadedChunks = 0

    for (let i = 0; i < chunks; i++) {
      const start = i * this.chunkSize
      const end = Math.min(start + this.chunkSize, file.size)
      const chunk = file.slice(start, end)

      try {
        await this.uploadChunk(chunk, i, chunks, file.name)
        uploadedChunks++
        const progress = (uploadedChunks / chunks) * 100
        this.updateProgress(progressBar, progress)
      } catch (error) {
        if (await this.handleUploadError(error, progressBar)) {
          i-- // Retry the current chunk
        } else {
          break
        }
      }
    }

    if (uploadedChunks === chunks) {
      await this.finalizeUpload(file.name, progressBar)
    }
  }

  async uploadChunk(chunk, chunkIndex, totalChunks, fileName) {
    const formData = new FormData()
    formData.append('chunk', chunk)
    formData.append('chunkIndex', chunkIndex)
    formData.append('totalChunks', totalChunks)
    formData.append('fileName', fileName)

    const response = await fetch('/upload/chunk', {
      method: 'POST',
      headers: {
        'X-CSRF-Token': this.csrfToken,
      },
      body: formData,
    })

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`)
    }

    return response.json()
  }

  async finalizeUpload(fileName, progressBar) {
    try {
      const response = await fetch('/upload/finalize', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': this.csrfToken,
        },
        body: JSON.stringify({ fileName }),
      })

      if (response.ok) {
        progressBar.classList.add('upload-complete')
        progressBar.textContent = `${fileName} uploaded successfully`
      } else {
        throw new Error('Failed to finalize upload')
      }
    } catch (error) {
      this.handleUploadError(error, progressBar)
    }
  }

  createProgressBar(fileName) {
    const progressBar = document.createElement('div')
    progressBar.classList.add('progress-bar')
    progressBar.dataset.fileName = fileName
    progressBar.textContent = `Uploading ${fileName}`
    this.progressContainer.appendChild(progressBar)
    return progressBar
  }

  updateProgress(progressBar, percent) {
    progressBar.style.width = `${percent}%`
    progressBar.textContent = `${progressBar.dataset.fileName} - ${percent.toFixed(1)}%`
  }

  async handleUploadError(error, progressBar) {
    console.error('Upload error:', error)
    progressBar.classList.add('upload-error')

    if (progressBar.dataset.retryCount === undefined) {
      progressBar.dataset.retryCount = '0'
    }

    const retryCount = parseInt(progressBar.dataset.retryCount)
    if (retryCount < this.retryAttempts) {
      progressBar.dataset.retryCount = (retryCount + 1).toString()
      progressBar.textContent = `Retrying... (${retryCount + 1}/${this.retryAttempts})`
      await new Promise((resolve) => setTimeout(resolve, 1000))
      progressBar.classList.remove('upload-error')
      return true
    }

    progressBar.textContent = 'Upload failed after multiple attempts'
    return false
  }

  showError(message) {
    const errorBar = document.createElement('div')
    errorBar.classList.add('progress-bar', 'upload-error')
    errorBar.textContent = message
    this.progressContainer.appendChild(errorBar)
    setTimeout(() => errorBar.remove(), 5000)
  }
}

// Initialize the uploader when the DOM is ready
document.addEventListener('DOMContentLoaded', () => new FileUploader())

```

<span aria-hidden="true" id="testing-the-file-uploader"></span>

## Testing the file uploader

To ensure reliable performance:

* Test with various file types and sizes.
* Verify the accuracy of chunked upload functionality.
* Simulate network errors to validate retry logic.
* Confirm proper CSRF token handling.
* Test the drag-and-drop functionality across different browsers.
* Check that progress indicators update accurately.
* Ensure compatibility with modern browsers such as Chrome, Firefox, Edge, and Safari.

<span aria-hidden="true" id="conclusion"></span>

## Conclusion

This modern implementation of a custom file uploader delivers a robust solution featuring chunked uploads, comprehensive error handling, and real-time progress tracking. By leveraging modern web APIs like Fetch, FormData, and the File API, you can provide a smooth upload experience while ensuring security with CSRF protection.

For a production-ready solution with additional features such as resumable uploads and cloud storage integration, consider using [Uppy 4.x⁠](https://uppy.io/docs/) (see the[Migration Guide⁠](https://uppy.io/blog/uppy-4.0/))—a powerful, open-source file uploader maintained by Transloadit.

\#file-upload#custom-file-uploader#javascript#html#user-interface#handling-uploads-service

### 👩‍💻 Join 20k+ developers

Sign up for our [monthly newsletter](/newsletters.md) to receive direct links to 3 exclusive tech — and 2 product updates. No less, no more.

Your email:

Get access

## File uploading and encoding. Made simple.

Transloadit streamlines file handling for developers, trusted by brands like Coursera and The New York Times. We’re known for a reliable API, top-notch support, and a strong commitment to open source, with projects like [Uppy⁠](https://uppy.io) and [Tus⁠](https://tus.io) setting standards in file processing.

[Sign up](/c/)[Book a Demo](https://survey.typeform.com/to/kRg47Xi5)

No credit card needed

Cancel anytime
