Dropzone.js turns a box on your page into a drag and drop upload area with previews and progress bars. In Laravel 13 you install it with npm, load it through Vite, point it to a route that validates and stores each file, and send the CSRF token in a request header. This tutorial builds the whole flow for image uploads.

Targets: Laravel 13 (PHP 8.3 to 8.5), Vite and the dropzone npm package. Check the Dropzone documentation for any option changes in the version you install.

Step 1: Install Dropzone

npm install dropzone 

Step 2: Import it in app.js

import Dropzone from 'dropzone'; import 'dropzone/dist/dropzone.css'; Dropzone.autoDiscover = false; const element = document.getElementById('image-dropzone'); if (element) { new Dropzone(element, { url: element.dataset.url, paramName: 'file', maxFilesize: 5, acceptedFiles: 'image/jpeg,image/png,image/webp', addRemoveLinks: true, headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, 'Accept': 'application/json', }, }); } 

Dropzone.autoDiscover = false stops Dropzone from attaching itself to every .dropzone element automatically, so your own setup is the only one that runs. maxFilesize is in megabytes. paramName is the form field name that arrives in Laravel, here file.

Step 3: Add the Blade page

<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>Upload images</title> @vite(['resources/js/app.js']) </head> <body> <form id="image-dropzone" class="dropzone" data-url="{{ route('uploads.store') }}"></form> </body> </html> 

The CSRF token is in a meta tag so the script can read it. Without it, uploads fail with a 419 error.

Step 4: Create the route and controller

use App\Http\Controllers\UploadController; use Illuminate\Support\Facades\Route; Route::view('/uploads', 'uploads'); Route::post('/uploads', [UploadController::class, 'store'])->middleware('auth')->name('uploads.store'); 
<?php namespace App\Http\Controllers; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; class UploadController extends Controller { public function store(Request $request): JsonResponse { $request->validate([ 'file' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'], ]); $path = $request->file('file')->store('uploads', 'public'); return response()->json(['path' => $path, 'url' => asset('storage/'.$path)]); } } 

Each dropped file is sent as its own request, so the controller handles one file at a time. Run php artisan storage:link once so the URL returned in the response can be opened in a browser. The server validation matches the client settings (5 MB, image types). Never rely on the browser checks alone.

How do I show server validation errors?

When validation fails, Laravel returns a 422 JSON response. Dropzone shows a generic error by default. Handle the error event to show Laravel's message:

const dropzone = new Dropzone(element, { /* same options as above */ }); dropzone.on('error', (file, response) => { const message = typeof response === 'string' ? response : (response.errors?.file?.[0] ?? response.message ?? 'Upload failed.'); file.previewElement.querySelector('.dz-error-message span').textContent = message; }); 

Use this version instead of the one in Step 2 if you want custom messages. Keep a reference to the Dropzone instance in a variable so you can attach events to it.

How do I delete an uploaded file with the remove link?

Save the stored path from the success response and send a delete request when the user removes the file:

dropzone.on('success', (file, response) => { file.serverPath = response.path; }); dropzone.on('removedfile', (file) => { if (!file.serverPath) return; fetch('/uploads', { method: 'DELETE', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content, }, body: JSON.stringify({ path: file.serverPath }), }); }); 

On the server, validate that the path starts with uploads/ and belongs to the current user before deleting it with Storage::disk('public')->delete($path). Never delete a path taken directly from the request without checking it, because a user could try to delete other files.

How do I check that it works?

  1. Run npm run dev, open /uploads while logged in and drag a JPG into the box.
  2. Check that a progress bar appears and the file is saved in storage/app/public/uploads.
  3. Drop a PDF and confirm that Dropzone rejects it. Then bypass the browser check with a tool such as curl and confirm that Laravel returns 422.

Common mistakes

  • 419 error: the CSRF header is missing.
  • Dropzone attached twice: set Dropzone.autoDiscover = false.
  • No styles: import the Dropzone CSS file.
  • Mismatched limits: align Dropzone's maxFilesize with Laravel's max and PHP's upload_max_filesize.
  • Trusting file types in the browser only: always validate on the server.

FAQ

Can I upload several files at once?

Yes. Dropzone sends each file as a separate request by default. Use uploadMultiple: true if you want one request with all files, and change the controller to accept an array.

Can Dropzone work inside a larger form?

Yes. A common pattern is to upload files immediately, collect the returned paths in hidden inputs, and submit them with the main form.

Is there an alternative?

Livewire has built-in file uploads, and plain HTML inputs work for simple cases. Dropzone is useful when you want previews and drag and drop without a framework.

References

Originally published on yasiramin.dev.