dasundev/livewire-dropzone
Livewire Dropzone adds drag-and-drop file uploads to your Livewire components with minimal setup. Easily handle dropped files, previews, and validation in your Laravel apps.
1. Install and verify the version you get.
composer require dasundev/livewire-dropzone
Then pin it in composer.json ("dasundev/livewire-dropzone": "^3.0") rather than floating. The changelog shows real breaking changes across majors — v2.0.0 removed the built-in dropzone label, and v1 → v2 changed markup/behavior. Know which major you're on before copying snippets from blog posts.
2. First place to look: the vendor Blade view.
The README is intentionally thin and the real documentation lives off-site at dasun.dev/docs/livewire-dropzone. Before writing code, open:
vendor/dasundev/livewire-dropzone/resources/views/
vendor/dasundev/livewire-dropzone/src/
That view is the authoritative list of props, slots, and CSS classes for your pinned version. It is faster and more reliable than guessing from the changelog, and it tells you exactly which dz- prefixed classes you can style.
3. First use case: a single avatar/profile photo in an existing form.
This is the lowest-risk integration because it exercises the whole path (drop → Livewire temp upload → validation → persist) without array handling.
use Livewire\Component;
use Livewire\WithFileUploads;
class ProfilePhoto extends Component
{
use WithFileUploads;
public $photo = null;
protected $rules = [
'photo' => 'required|image|max:2048',
];
public function save()
{
$this->validate();
$path = $this->photo->store('profile-photos', 'public');
auth()->user()->update(['photo' => $path]);
$this->reset('photo');
}
}
<form wire:submit="save">
<livewire:dropzone
wire:model="photo"
:rules="['image', 'max:2048']"
:multiple="false"
/>
@error('photo')
<span class="text-red-600">{{ $message }}</span>
@enderror
<button type="submit">Save</button>
</form>
Two things to internalize from day one:
:rules prop is a client/UX convenience applied by the component before it syncs the file upward. Your server-side $rules and $this->validate() are the actual security boundary.= null, = []). Early releases had bugs around uninitialized $file/$files (fixed in 1.0.5 and 2.0.1); initializing costs nothing and avoids resurrecting those failures.If you only remember one thing: you are not uploading through Dropzone.js — you are uploading through Livewire's temporary file upload pipeline. Every Livewire limit, disk config, and cleanup rule applies.
use Livewire\WithFileUploads;
public $files = [];
protected $rules = [
'files' => 'array|max:10',
'files.*' => 'image|max:5120',
];
<livewire:dropzone
wire:model="files"
:rules="['image', 'max:5120']"
:multiple="true"
/>
Persist in one place, never in a loop inside render():
public function save()
{
$this->validate();
foreach ($this->files as $file) {
$this->post->attachments()->create([
'path' => $file->store('attachments', 'public'),
]);
}
$this->reset('files');
}
Treat the dropzone as a form field, not an uploader. Files sit in Livewire's temp directory until the user hits Save. Benefits: the user can correct mistakes, you validate once, and a half-finished form doesn't litter storage. This is the right choice for 90% of forms.
updatedFiles()When the UX demands immediate persistence (a media manager, a chat attachment), hook the property update:
public function updatedFiles()
{
$this->validate();
foreach ($this->files as $file) {
Media::create([
'disk' => 'public',
'path' => $file->store('media', 'public'),
]);
}
$this->reset('files');
}
Don't mix this with a submit-time save() that also iterates $files, or you'll double-insert. Pick one ownership model per screen.
Model it as "new upload wins, otherwise keep existing":
public $photo = null; // new upload
public $existingPhoto; // current path from the DB
public function save()
{
$this->validateOnly('photo'); // nullable on edit screens
$path = $this->photo
? $this->photo->store('profile-photos', 'public')
: $this->existingPhoto;
auth()->user()->update(['photo' => $path]);
$this->reset('photo');
}
Keep the required rule off the edit form, or the user is forced to re-upload just to change their name.
The component is unique by default (since 1.0.2) and UUID dispatch was fixed in 1.0.9, but you should still give each instance a stable identity when you hand-write markup or duplicate a Blade partial:
<livewire:dropzone wire:model="cover" :multiple="false" wire:key="dz-cover" />
<livewire:dropzone wire:model="gallery" :multiple="true" wire:key="dz-gallery" />
If two dropzones ever appear to share files, the first thing to check is duplicated wire:key/name values, not the package.
:rules for instant feedback; keep $rules on the server. Never derive authorization from the :rules prop.$file->store('x', 's3') keeps you portable; hardcoded public_path() breaks in containers.save() will make the Livewire request time out on large images.wire:model support for a FormObject file property; keep $files as a plain array on the component and move paths into the form object during save().dz- classes. Style via the documented dz- prefixed selectors in your own stylesheet (see Gotchas on the styles pipeline).max: rule is kilobytes'max:2048' is ~2MB, not 2048MB. This is the single most common bug report on file-upload packages. Also confirm PHP agrees:
upload_max_filesize = 12M
post_max_size = 12M
Livewire's own config/livewire.php (temporary_file_upload.rules, max_upload_time, directory, disk) can reject a file before your component ever sees it — and the failure surfaces as a generic validation error that doesn't match your $rules message.
Everything transits Livewire's temporary upload endpoint. For multi-hundred-MB files this means memory limits, request timeouts, and a temp disk with enough space. If your product needs large uploads or presigned S3 URLs, this package is the wrong tool (or you need its chunking support — verify against the docs for your version). Keep the dropzone for images and documents in the low tens of MB.
Since 1.0.6 the remove event handles cleanup. If you add your own removal handler and also call unlink()/Storage::delete() on the temp path, you'll race Livewire's cleanup. Let the component own removal; you only own the persisted file, and even then, delete the DB record first and the file in a deleted observer/job.
Read CHANGELOG.md before every minor bump; this package's releases are mostly dependency bumps, so behavior changes hide in the majors.
The styles ship via a dedicated JS/PostCSS resources package (resources/js/livewire-dropzone-styles) and historically had a dz- class prefix fix (1.0.1). Consequences:
dz- selectors. Don't edit vendor files; you'll lose it on composer update.Always re-validate server-side, and validate files.* (not just files) for arrays. Also authorize: a dropzone in a Blade view is not an access check. Confirm the user may attach to this record before storing.
Don't try to test drag-and-drop in PHP. Test the server contract:
Livewire::test(ProfilePhoto::class)
->set('photo', UploadedFile::fake()->image('avatar.jpg'))
->call('save')
->assertHasNoErrors();
Storage::fake('public');
Use UploadedFile::fake()->create('doc.pdf', 5000) to exercise the size rule. For the JS side, assert in the browser layer (Dusk/Playwright) only if the drag interaction itself is a product requirement.
Drag-and-drop is the headline feature but never the only path — confirm click-to-browse works, keep a visible label/instructions (remember v2 removed the built-in label), associate @error output near the dropzone, and test on touch devices where drag events don't exist. Long filenames were fixed to wrap in 2.0.4, but very long names in your own surrounding UI still need break-words.
$rules error → check livewire.temporary_file_upload.rules and PHP post_max_size.null after dropping → check the wire:model name matches the public property exactly, and that the property is initialized.wire:key/name; you're on ≥1.0.9.save() sees an empty array → don't add wire:model.defer/.live modifiers assuming they help; the component decides when it syncs.updatedFiles() and save().How can I help you explore Laravel packages today?