webdevpuneet.com

Bootstrap Multi-File Upload Queue with Drag & Drop

Bootstrap Multi-File Upload Queue · Forms · Plain HTML, CSS & JS · Live preview

CategoryForms

Copy & paste

Source Code

<div class="bsqueue-page">
  <div class="card bsqueue-card">
    <div class="card-body p-4">
      <div class="d-flex align-items-center gap-3 mb-3">
        <span class="bsqueue-icon" aria-hidden="true">
          <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4"/><path d="M7 9l5-5 5 5"/><path d="M4 16v3a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-3"/></svg>
        </span>
        <div>
          <h6 class="mb-0 fw-bold">Upload files</h6>
          <div class="small text-muted">Each file uploads on its own</div>
        </div>
      </div>

      <label class="bsqueue-zone" id="bsqueueZone">
        <input type="file" id="bsqueueInput" class="visually-hidden" multiple>
        <span class="fw-semibold">Drag &amp; drop files here</span>
        <span class="small text-muted">or <span class="bsqueue-link">browse</span> &middot; up to 10 MB each</span>
      </label>

      <div class="d-flex justify-content-between align-items-center mt-3 mb-1">
        <span class="small fw-semibold" id="bsqueueSummary" aria-live="polite"></span>
        <button type="button" class="btn btn-link btn-sm p-0 text-decoration-none d-none" id="bsqueueClear">Clear completed</button>
      </div>
      <ul class="list-unstyled mb-0" id="bsqueueList"></ul>
    </div>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Every file has its own progress state and timer, so rows never affect each other
Drag-and-drop zone built on a real, keyboard-focusable file input with a visible focus ring
Color-coded file-type tiles (PDF, image, document, spreadsheet, archive) with the extension
Striped animated Bootstrap progress bars that turn green when done and red when failed
Per-row Cancel, Retry and Remove buttons that show only when they make sense
10 MB size check with an inline "Too large" row and no pointless Retry
File names set with textContent, so a malicious file name can never inject HTML
Live "X of Y uploaded" summary, "Clear completed" button and an empty state

About this UI Snippet

Bootstrap Multi-File Upload Queue — Drag & Drop, Per-File Progress, Cancel and Retry

Screenshot of the Bootstrap Multi-File Upload Queue snippet rendered live

A multi-file upload queue lets a visitor add several files at once and watch each one upload on its own, cancel one without stopping the rest, and retry anything that failed. This snippet builds that pattern with the real Bootstrap 5.3 card, progress bar and button classes, plus a drag-and-drop zone, file-type tiles and a live summary line, in plain JavaScript with no upload library.

One object and one timer per file

Every selected file becomes its own object in a files array: { id, file, progress, status, timer, retryable, error }. Each one gets its own setInterval from runUpload(f). Five files means five independent timers ticking at their own random pace, so one file finishing, failing or being cancelled has no effect on any other row. A single shared progress value could never represent several uploads honestly.

A drop zone that is also a real file input

The zone is a <label> wrapped around a visually hidden <input type="file" multiple>. Clicking it opens the file picker, and the input stays keyboard-focusable (Bootstrap's visually-hidden class, not display:none), with a focus ring through :focus-within. The dragenter, dragover, dragleave and drop handlers add a highlight class and pass the dropped files to the same addFiles() function the picker uses, so both input methods behave identically.

Row states: uploading, done, failed

Each row shows a colored file-type tile (PDF, image, document, spreadsheet, archive), the file name, its size and a status line. While uploading, the Bootstrap bar is striped and animated with a Cancel button. When it finishes the bar turns bg-success and the status reads "Uploaded". A cancelled file turns bg-danger and gains Retry and Remove buttons. Retry restarts only that file from zero.

Validation before upload

MAX_BYTES rejects files over 10 MB before any upload starts. The row appears immediately with a red bar and "Too large (max 10 MB)", and no Retry button, because retrying the same file can't succeed. Change the constant, or check file.type too, to match your server rules.

Safe file names

File names are written into the row with textContent, never concatenated into innerHTML. A file literally named <img src=x onerror=alert(1)>.jpg therefore shows as plain text instead of running as HTML, which matters because users control file names.

A summary that cannot drift

updateSummary() recounts files.filter(f => f.status === 'done') every time a row changes, instead of keeping a separate counter, so "2 of 4 uploaded" always matches the rows. When at least one file is done, a "Clear completed" button appears, and an empty state replaces the list when nothing is left.

Connecting a real upload

The demo rows at the bottom of the script are placeholders to delete. Replace the interval in runUpload() with a per-file XMLHttpRequest (use upload.onprogress for the percentage) or fetch with an AbortController, and have Cancel call abort(). The rendering, retry and summary code doesn't need to change. Pair this with bootstrap-file-upload-drag-drop for a single-file version, or bootstrap-file-upload-progress to study one progress bar in isolation.

Step by step

How to Use

  1. 1
    Drop or browse for several filesDrag files onto the dashed zone or click it. Each file gets its own row with a type tile, size and progress bar, all uploading in parallel.
  2. 2
    Watch the summary lineIt updates as each file finishes, for example "2 of 4 uploaded", and a "Clear completed" button appears.
  3. 3
    Click "Cancel" on one file mid-uploadOnly that row stops and turns red. Every other file keeps uploading.
  4. 4
    Retry or remove a cancelled fileRetry restarts just that file from zero; Remove deletes the row and updates the count.
  5. 5
    Try a file over 10 MBIt is rejected straight away with "Too large", with no upload and no Retry button.
  6. 6
    Connect your endpointDelete the demo rows and replace the timer in runUpload() with an XMLHttpRequest or fetch call per file.

Real-world uses

Common Use Cases

Bulk document or photo upload forms
The natural next step from bootstrap-file-upload-progress once a form accepts more than one file per submission, such as claims, applications or onboarding packs.
Media libraries and asset managers
Upload a batch of images, PDFs and archives at once with independent, cancellable progress per file and a clear failed state for oversized assets.
DEV
Support and bug report attachments
Let users drop several screenshots and logs into a ticket, retry the ones that failed, and clear the finished ones. Pair it with bootstrap-file-upload-drag-drop for a simpler single-file zone.
Learning independent per-item async state
A clear example of managing several unrelated async operations at once, with per-item status, cancel, retry and a derived summary that never drifts.

Got questions?

Frequently Asked Questions

Yes. Each file gets its own setInterval the moment it is added, with no waiting for a previous file. That matches how a real version using several concurrent fetch or XHR requests would behave.

Cancelling stops that file's timer, marks it failed with "Cancelled" and turns its bar red. You can then Retry it from zero or Remove it. No other row is affected.

Change the MAX_BYTES constant, and add a check on file.type or the extension inside addFiles(). Rejected files are shown as failed rows with a message and no Retry button.

Replace the setInterval simulation inside runUpload() with a per-file XMLHttpRequest, using upload.onprogress for the percentage, or fetch with an AbortController. Cancel then calls abort(); the rest of the row logic stays the same.

Yes. Keep the files array in component state, keep each file's request or timer in a ref-backed map keyed by id so re-renders do not restart it, and derive the summary count from the same array on every render.

Design your website