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.autoDiscoverstill auto-attaches to.dropzoneelements unless you switch it off. - Client-side
maxFilesizeandacceptedFilesare UX only — always re-validate type and size on the server. - Use
chunking: truefor 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 dropzoneThen 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
- Copy to clipboard with JavaScript — give users one-click sharing of uploaded file URLs.
- Image cropping in jQuery with Jcrop — let users crop images before they upload.
- Event delegation in jQuery — handle dynamically added preview and remove buttons cleanly.
- Minify CSS/JS and compile Sass with Gulp.js — bundle Dropzone's assets for production.
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.