Drag-and-Drop Multiple File Uploads with Dropzone.js

Blog / JavaScript · July 20, 2020 · Updated June 10, 2026 · 9 min read
Drag-and-Drop Multiple File Uploads with Dropzone.js

To add drag-and-drop multiple file uploads, point the Dropzone.js library (v6+) at any element and an upload URL, and it turns that element into a drop area that previews files and uploads them over AJAX — no jQuery required. The shortest version is one line: new Dropzone("#upload", { url: "/file/upload/", maxFiles: 10 }).

Dropzone owns the browser experience (drag-and-drop, thumbnails, progress bars, per-file validation). You still write the server endpoint that receives the multipart upload, re-validates it, and stores it safely.

Key takeaways

  • Install with npm install dropzone (or a CDN <link> + <script>) and import both the JS and the CSS.
  • Initialise programmatically: new Dropzone(element, { url, maxFiles, maxFilesize, acceptedFiles, parallelUploads }).
  • v6 dropped the old jQuery plugin syntax; Dropzone.autoDiscover still auto-attaches to .dropzone elements unless you switch it off.
  • Client-side maxFilesize and acceptedFiles are UX only — always re-validate type and size on the server.
  • Use chunking: true for large files, and store uploads outside the web root or in object storage such as Amazon S3.

What is Dropzone.js?

Dropzone.js is a lightweight, dependency-free JavaScript library that turns any DOM element — a <div> or a <form> — into a drag-and-drop file-upload zone. When a user drags files onto the element (or clicks it to open the file picker), Dropzone shows a preview for each file, runs your size and type rules, and uploads the files to your server with XMLHttpRequest.

The actively maintained package lives at dropzone/dropzone on npm, the successor to the original dropzonejs/dropzone project. The current major line is v6, written in modern ES modules and no longer shipping a jQuery plugin.

Install Dropzone.js (npm or CDN)

For a bundled app (Vite, webpack, SvelteKit, Rails, and so on), install from npm and import both the script and its stylesheet:

npm install --save dropzone
# or
yarn add dropzone
# or
pnpm add dropzone

Then import the Dropzone class and the default theme:

// app.js
import { Dropzone } from "dropzone";
import "dropzone/dist/dropzone.css"; // base styling + the drop-area look

// Stop Dropzone auto-attaching so we control initialisation ourselves.
Dropzone.autoDiscover = false;

No build step? Use the CDN — add these two tags to your page (stylesheet in the <head>, script before </body>):

<link rel="stylesheet" href="https://unpkg.com/dropzone@6/dist/dropzone.css" />
<script src="https://unpkg.com/dropzone@6/dist/dropzone-min.js"></script>

The fastest setup: a .dropzone form

The simplest integration needs zero JavaScript. Give a <form> the class dropzone and an action URL — Dropzone's auto-discovery finds it on page load and wires everything up:

<form action="/file/upload/" class="dropzone" id="my-dropzone">
  <!-- Dropzone draws the drop area and file previews inside this form. -->
</form>

Auto-discovery is convenient but global. To pass options to an auto-discovered form, attach them to Dropzone.options.<CamelCasedId> (so id="my-dropzone" becomes Dropzone.options.myDropzone). For anything beyond the basics, prefer the programmatic API below and switch auto-discovery off with Dropzone.autoDiscover = false to avoid the "Dropzone already attached" error.

Programmatic setup with the v6 API

Creating the instance yourself gives full control over configuration and events. Pass a selector (or element) and an options object to the Dropzone constructor:

/**
 * Multi-file drag-and-drop uploader.
 * @see https://docs.dropzone.dev
 */
const uploader = new Dropzone("#upload", {
  url: "/file/upload/",          // required: where files are POSTed
  method: "post",
  paramName: "file",             // form field name (default: "file")
  maxFiles: 10,                  // max number of files in the drop area
  maxFilesize: 5,                // per-file size cap, in MB
  acceptedFiles: "image/*,.pdf", // MIME types and/or extensions
  parallelUploads: 3,            // how many upload at once
  uploadMultiple: false,         // one request per file (true => file[0], file[1]...)
  createImageThumbnails: true,   // preview thumbnails for images
  addRemoveLinks: true,          // show a "Remove file" link on each preview
  dictDefaultMessage: "Drag files here or click to upload",
});

Key configuration options

These are the options you will reach for most often:

Option What it does
url Endpoint that receives the upload (required for non-form elements).
paramName Field name for the file part; defaults to file.
maxFiles Maximum number of files allowed in the drop area.
maxFilesize Per-file size limit in MB (client-side check only).
acceptedFiles Comma-separated MIME types/extensions, e.g. image/*,.pdf.
parallelUploads How many files upload concurrently from the queue.
uploadMultiple Send several files in one request (fields become file[0], file[1]...).
autoProcessQueue If false, call uploader.processQueue() yourself (e.g. on submit).
addRemoveLinks Adds a remove link to each preview.
chunking Enables chunked uploads for large files.
headers Extra request headers, e.g. a CSRF token.

Previews, validation, progress, and removing files

Dropzone renders a preview (with a thumbnail for images) for every accepted file and rejects anything that fails maxFilesize or acceptedFiles, showing the error on the preview. Hook into its events to drive your own UI:

uploader.on("addedfile", (file) => {
  console.log(`Queued ${file.name} (${(file.size / 1024).toFixed(0)} KB)`);
});

uploader.on("uploadprogress", (file, percent) => {
  // Drive a custom progress bar if you aren't using Dropzone's default.
});

uploader.on("success", (file, response) => {
  // `response` is the JSON your server returned, already parsed.
  console.log("Stored at", response.files);
});

uploader.on("error", (file, message) => {
  console.warn(`${file.name} failed:`, message);
});

uploader.on("removedfile", (file) => {
  // Optionally tell the server to delete an already-uploaded file.
});

uploader.on("maxfilesexceeded", (file) => uploader.removeFile(file));

Chunked uploads for large files

For large files (video, datasets) or flaky connections, enable chunking. Dropzone slices each file into smaller parts that upload sequentially, and your server reassembles them once the final chunk arrives:

const uploader = new Dropzone("#upload", {
  url: "/file/upload/",
  chunking: true,             // turn on chunked uploads
  forceChunking: true,        // chunk even small files (consistent server logic)
  chunkSize: 2 * 1024 * 1024, // 2 MB per chunk
  parallelChunkUploads: false,
  retryChunks: true,
  retryChunksLimit: 3,
  // Runs after the last chunk; tell the server to stitch the parts together.
  chunksUploaded: async (file, done) => {
    await fetch(`/file/complete/?uuid=${file.upload.uuid}`, { method: "POST" });
    done(); // signal Dropzone the upload is finished
  },
});

Each chunk POST includes dzuuid, dzchunkindex, dztotalchunkcount, and dzchunkbyteoffset, so your endpoint can write each part at the right offset and merge them when the last chunk lands.

Handling the upload on the server (Django)

Dropzone sends a standard multipart/form-data POST, so any framework can read it. Here is a minimal Django view that accepts the files, re-validates them, stores them, and returns JSON (Dropzone parses the JSON and exposes it on the success event):

# views.py
from django.core.files.storage import default_storage
from django.http import JsonResponse
from django.views.decorators.http import require_POST

ALLOWED_TYPES = {"image/png", "image/jpeg", "application/pdf"}
MAX_BYTES = 5 * 1024 * 1024  # 5 MB - keep in sync with maxFilesize on the client


@require_POST
def upload(request):
    # Dropzone posts one file per request as "file" by default. With
    # uploadMultiple: true the parts arrive as file[0], file[1], ...
    files = request.FILES.getlist("file") or request.FILES.getlist("file[]")
    saved = []
    for f in files:
        # NEVER trust the client: re-check size and content type here.
        if f.size > MAX_BYTES:
            return JsonResponse({"error": f"{f.name} is too large"}, status=400)
        if f.content_type not in ALLOWED_TYPES:
            return JsonResponse({"error": f"{f.name}: type not allowed"}, status=415)
        # Store outside the web root or push to object storage (e.g. S3).
        key = default_storage.save(f"uploads/{f.name}", f)
        saved.append(default_storage.url(key))
    return JsonResponse({"files": saved})

Sending Django's CSRF token

Django rejects unauthenticated POSTs, so send the CSRF token as a header. Add it to the Dropzone headers option, reading the value from the cookie:

/**
 * Read a cookie value by name.
 * @param {string} name
 * @returns {string}
 */
function getCookie(name) {
  const match = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"));
  return match ? decodeURIComponent(match[2]) : "";
}

const uploader = new Dropzone("#upload", {
  url: "/file/upload/",
  headers: { "X-CSRFToken": getCookie("csrftoken") },
});

Security: never trust the client

Client-side maxFilesize and acceptedFiles only improve UX — an attacker can POST straight to your URL and skip them. Lock the endpoint down:

  • Re-validate type and size on the server. Check the real content type (ideally sniff the magic bytes), not just the extension or the client-sent MIME.
  • Never store uploads in the web root. Keep them outside the served directory, or push them to object storage like Amazon S3 and serve through signed URLs.
  • Sanitise file names and generate your own keys (for example a UUID) to avoid path traversal and collisions.
  • Cap counts and total size per request and per user, and require authentication where appropriate.
  • Scan untrusted files for malware before you make them downloadable.

If you process or migrate large volumes of user uploads, our team can help you design the storage and delivery pipeline — see our web development services.

Dropzone.js vs FilePond vs Uppy vs native upload

All four can do multi-file drag-and-drop. Choose based on dependencies, built-in UI, and whether you need resumable uploads:

Option Bundle / deps Built-in UI & previews Chunked / resumable Best for
Dropzone.js v6+ tiny, no deps Yes Chunked (manual server merge) Drop-in multi-file uploads with minimal code
FilePond small core + optional plugins Yes, polished Via chunk plugin Image-heavy forms needing edit/validate plugins
Uppy modular, no deps Yes (Dashboard) Yes (tus, resumable) Large/remote files, S3, Drive/Dropbox sources
Native <input type="file" multiple> + Fetch 0 KB No (build your own) Manual Full control with zero dependencies

No library? Native HTML5 drag-and-drop + Fetch

If you only need a basic drop area, the browser already supports it. Handle the dragover and drop events, then send the files with FormData and fetch — no dependency required. You do trade away previews, progress UI, and per-file validation, which you would build yourself:

const drop = document.querySelector("#drop");

drop.addEventListener("dragover", (e) => {
  e.preventDefault(); // required so the browser allows a drop
  drop.classList.add("is-over");
});

drop.addEventListener("dragleave", () => drop.classList.remove("is-over"));

drop.addEventListener("drop", async (e) => {
  e.preventDefault();
  drop.classList.remove("is-over");

  const body = new FormData();
  for (const file of e.dataTransfer.files) body.append("file", file);

  // Don't set Content-Type yourself - the browser adds the multipart boundary.
  const res = await fetch("/file/upload/", { method: "POST", body });
  const data = await res.json();
  console.log("Uploaded:", data.files);
});

Related guides

MicroPyramid has shipped 50+ products over 12+ years (since 2014); secure, scalable file handling is part of nearly every one.

Frequently Asked Questions

How do I allow multiple files with Dropzone.js?

Dropzone accepts multiple files by default. Use maxFiles to cap the count (e.g. maxFiles: 10) and parallelUploads to control how many upload at once. Each file is sent in its own request by default; set uploadMultiple: true to send several files in a single request, where the fields arrive as file[0], file[1], and so on.

Does Dropzone.js still need jQuery?

No. Dropzone.js v6 is dependency-free and written as ES modules. The old jQuery plugin form, $("#el").dropzone({...}), was removed — initialise with new Dropzone(element, options) instead. Auto-discovery via Dropzone.autoDiscover still works for .dropzone form elements.

How do I limit file types and sizes?

Set acceptedFiles to a comma-separated list of MIME types or extensions (for example "image/*,.pdf") and maxFilesize to the per-file limit in megabytes. These are client-side conveniences only — always re-check the type and size on the server, since anyone can POST directly to your upload URL.

How do I upload large files with Dropzone.js?

Enable chunking: true (optionally forceChunking: true) and set chunkSize in bytes. Dropzone splits each file into parts that upload sequentially and adds dzuuid, dzchunkindex, and dztotalchunkcount to each request so your server can reassemble them. Use the chunksUploaded callback to finalise the file after the last chunk.

How do I let users remove a file from the drop area?

Set addRemoveLinks: true to show a remove link on each preview, and customise the label with dictRemoveFile. To remove a file programmatically call uploader.removeFile(file) (or uploader.removeAllFiles()); listen for the removedfile event if you also need to delete an already-uploaded file from the server.

Why aren't my files uploading to the server?

The most common cause is a missing or wrong url, a CSRF/auth failure (send the token via headers), or a server that doesn't return JSON. Check the network tab: Dropzone needs a 2xx response, and reads files from the file field (or file[0], file[1]... when uploadMultiple is true). Server-side maxFilesize/upload limits can also silently reject large files.

Share this article