File upload functionality is essential for modern web applications. This tutorial guides you through creating a secure and user-friendly HTML file upload form with features like multiple file selection, progress tracking, and drag-and-drop support.

Introduction to HTML file upload forms

HTML file upload forms enable users to select and upload files to a server. Modern web applications require robust file upload capabilities for features like profile pictures, document submissions, and media sharing. Let's explore how to implement this functionality effectively.

Setting up a basic file upload form in HTML

Create a basic HTML file upload form using the <form> element with an <input type="file"> field:

<form action="/upload" method="post" enctype="multipart/form-data">
  <label for="file-upload" class="form-label">Select file:</label>
  <input
    type="file"
    id="file-upload"
    name="file"
    accept=".jpg,.jpeg,.png,.pdf"
    aria-describedby="file-help"
  />
  <div id="file-help" class="form-text">Accepted formats: JPG, PNG, PDF (max 10MB)</div>
  <button type="submit">Upload File</button>
</form>

Key attributes:

  • enctype="multipart/form-data" enables file uploads
  • accept attribute limits file types
  • ARIA attributes improve accessibility

Handling single and multiple file uploads

Enable multiple file selection and implement progress tracking:

<form id="upload-form" action="/upload" method="post" enctype="multipart/form-data">
  <input type="file" id="file-upload" name="files[]" multiple accept=".jpg,.jpeg,.png,.pdf" />
  <div id="upload-progress" class="progress" hidden>
    <div class="progress-bar" role="progressbar"></div>
  </div>
  <button type="submit">Upload Files</button>
</form>

<script>
  const form = document.getElementById('upload-form')
  const progress = document.getElementById('upload-progress')
  const progressBar = progress.querySelector('.progress-bar')

  form.addEventListener('submit', async (e) => {
    e.preventDefault()
    const formData = new FormData(form)
    progress.hidden = false

    try {
      const response = await fetch('/upload', {
        method: 'POST',
        body: formData,
        signal: AbortSignal.timeout(30000), // 30-second timeout
      })

      if (!response.ok) throw new Error(`HTTP error: ${response.status}`)
      const result = await response.json()
      console.log('Upload successful:', result)
    } catch (error) {
      console.error('Upload failed:', error)
      alert('Upload failed. Please try again.')
    } finally {
      progress.hidden = true
    }
  })

  // Track upload progress using XMLHttpRequest for more granular control
  const xhr = new XMLHttpRequest()
  xhr.upload.addEventListener('progress', (e) => {
    if (e.lengthComputable) {
      const percentComplete = (e.loaded / e.total) * 100
      progressBar.style.width = percentComplete + '%'
      progressBar.setAttribute('aria-valuenow', percentComplete)
    }
  })
</script>

Considerations:

  • For very large files, consider implementing chunked uploads and a cancel upload option.
  • For production use, ensure server-side support for progress tracking and file validation.

Customizing the file upload button

Create a modern, styled file upload interface:

<div class="upload-container">
  <label for="file-upload" class="upload-button">
    <svg aria-hidden="true" width="24" height="24">
      <path d="M12 2l4 4h-3v9h-2V6H8l4-4z" />
    </svg>
    Choose Files
  </label>
  <input type="file" id="file-upload" multiple hidden />
  <div id="file-list" class="file-list"></div>
</div>

<style>
  .upload-button {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 8px 16px;
    background: #0066cc;
    color: white;
    border-radius: 4px;
    cursor: pointer;
    transition: background 0.2s;
  }

  .upload-button:hover {
    background: #0052a3;
  }

  .file-list {
    margin-top: 16px;
    padding: 8px;
    border: 1px solid #ddd;
    border-radius: 4px;
  }
</style>

<script>
  const fileInput = document.getElementById('file-upload')
  const fileList = document.getElementById('file-list')

  fileInput.addEventListener('change', () => {
    fileList.innerHTML = ''
    Array.from(fileInput.files).forEach((file) => {
      const item = document.createElement('div')
      item.textContent = `${file.name} (${formatFileSize(file.size)})`
      fileList.appendChild(item)
    })
  })

  function formatFileSize(bytes) {
    if (bytes === 0) return '0 B'
    const k = 1024
    const sizes = ['B', 'KiB', 'MiB', 'GiB']
    const i = Math.floor(Math.log(bytes) / Math.log(k))
    return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
  }
</script>

Adding client-side validation

Implement comprehensive file validation to improve user experience and security:

<script>
  const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB
  const ALLOWED_TYPES = {
    'image/jpeg': ['.jpg', '.jpeg'],
    'image/png': ['.png'],
    'application/pdf': ['.pdf'],
  }

  function validateFile(file) {
    const errors = []

    if (file.size > MAX_FILE_SIZE) {
      errors.push(`File ${file.name} exceeds 10MB limit`)
    }

    if (!Object.keys(ALLOWED_TYPES).includes(file.type)) {
      errors.push(`File ${file.name} has unsupported type: ${file.type}`)
    }

    return errors
  }

  function formatFileSize(bytes) {
    if (bytes === 0) return '0 B'
    const k = 1024
    const sizes = ['B', 'KiB', 'MiB', 'GiB']
    const i = Math.floor(Math.log(bytes) / Math.log(k))
    return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
  }

  function updateFileList() {
    const fileList = document.getElementById('file-list')
    fileList.innerHTML = ''
    Array.from(fileInput.files).forEach((file) => {
      const item = document.createElement('div')
      item.textContent = `${file.name} (${formatFileSize(file.size)})`
      fileList.appendChild(item)
    })
  }

  fileInput.addEventListener('change', () => {
    const errors = []
    Array.from(fileInput.files).forEach((file) => {
      errors.push(...validateFile(file))
    })

    if (errors.length > 0) {
      alert(errors.join('\n'))
      fileInput.value = ''
      return
    }

    // Proceed with valid files
    updateFileList()
  })
</script>

Implementing drag-and-drop file upload

Add modern drag-and-drop functionality with enhanced accessibility and error handling:

<div
  id="drop-zone"
  class="drop-zone"
  role="button"
  tabindex="0"
  aria-label="Drop files here or click to select"
>
  <div class="drop-zone-content">
    <svg aria-hidden="true" width="48" height="48">
      <path d="M24 4l8 8h-6v18h-4V12h-6l8-8z" />
    </svg>
    <p>Drop files here or click to select</p>
  </div>
</div>

<style>
  .drop-zone {
    padding: 24px;
    border: 2px dashed #ccc;
    border-radius: 8px;
    text-align: center;
    transition: border-color 0.2s;
    cursor: pointer;
  }

  .drop-zone.drag-over {
    border-color: #0066cc;
    background: rgba(0, 102, 204, 0.1);
  }
</style>

<script>
  const dropZone = document.getElementById('drop-zone')

  function handleDrop(e) {
    e.preventDefault()
    e.stopPropagation()
    try {
      const dt = e.dataTransfer
      const files = dt.files
      handleFiles(files)
    } catch (error) {
      alert('An error occurred during file drop: ' + error.message)
    } finally {
      dropZone.classList.remove('drag-over')
    }
  }

  function handleFiles(files) {
    const errors = []
    Array.from(files).forEach((file) => {
      errors.push(...validateFile(file))
    })

    if (errors.length > 0) {
      alert(errors.join('\n'))
      return
    }

    // Update file input and list
    const transfer = new DataTransfer()
    Array.from(files).forEach((file) => transfer.items.add(file))
    fileInput.files = transfer.files
    updateFileList()
  }

  // Event listeners
  dropZone.addEventListener('dragover', (e) => {
    e.preventDefault()
    dropZone.classList.add('drag-over')
  })

  dropZone.addEventListener('dragleave', () => {
    dropZone.classList.remove('drag-over')
  })

  dropZone.addEventListener('drop', handleDrop)

  // Keyboard accessibility
  dropZone.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault()
      fileInput.click()
    }
  })

  // Touch device support
  dropZone.addEventListener('touchend', (e) => {
    e.preventDefault()
    fileInput.click()
  })
</script>

Security considerations

It is essential to validate and sanitize file uploads on the server side as well. Enforce file type restrictions, limit file sizes on both client and server, and sanitize file names to prevent potential security vulnerabilities. Additionally, consider integrating antivirus scanning, using HTTPS for file transmissions, and keeping your server software up to date.

Conclusion

This tutorial demonstrates how to create a modern, accessible file upload form with enhancements such as progress tracking, client-side validation, and drag-and-drop support. For very large files, consider implementing chunked uploads and a cancel upload option to improve user experience and resilience. Additionally, always perform robust server-side validations and security checks.

For a more robust solution with resumable uploads and comprehensive progress tracking, consider using Uppy 4.x, an open-source file uploader that works seamlessly with modern web applications.