anil/file-picker
Laravel Livewire media library & file picker modal for any file type. Upload via drag/drop or paste, search/filter, tags/folders/favorites, single or multi-select, trash/restore, replace files, SHA-256 dedupe, quotas, stats, downloads, and commands.
A powerful media library and file picker component for Laravel Livewire. Supports images, videos, audio, documents, and every other file type, all behind a polished modal interface.

max_filesreuse, reject, or allow strategiesuser_id, optionally scope per userFilePicker::getStats()file-picker:prune-trash, file-picker:prune-orphans, file-picker:statsplank/laravel-mediable ^6.0 — installed automaticallycomposer require anil/file-picker
php artisan file-picker:install
file-picker:install publishes config/file-picker.php and runs an additive migration that adds the columns the package needs (folder, tags, is_favorite, hash, width, height, duration, user_id, download_count, custom_properties, deleted_at) to Plank's existing media table. CSS/JS are served via a built-in route — nothing to publish, nothing to compile.
php artisan file-picker:install --force # overwrite already-published files
php artisan file-picker:install --no-migrate # publish only, skip migrations
php artisan file-picker:install --views # publish blade views for UI overrides
php artisan file-picker:install --lang # publish language files
php artisan file-picker:install --assets # publish CSS/JS to public/ (optional)
The component pushes CSS to @stack('head') and JS to @stack('scripts'):
<!DOCTYPE html>
<html>
<head>
@stack('head')
</head>
<body>
{{ $slot }}
@stack('scripts')
</body>
</html>
{{-- Single file --}}
<livewire:file-picker input-name="featured_image" />
{{-- Multiple files, up to 5 --}}
<livewire:file-picker input-name="gallery" :multiple="true" :max-files="5" />
{{-- Restrict to specific file types --}}
<livewire:file-picker input-name="avatar" :allowed-types="['image']" />
namespace App\Livewire;
use Livewire\Attributes\On;
use Livewire\Component;
class PostForm extends Component
{
public array $selectedMedia = [];
#[On('filesSelected')]
public function handleFilesSelected(array $selected, string $inputName): void
{
// $selected is an array of media IDs
$this->selectedMedia = $selected;
}
public function render()
{
return view('components.post-form');
}
}
<div>
<livewire:file-picker
input-name="media"
:multiple="true"
:max-files="10"
:selected="$selectedMedia"
:allowed-types="['image', 'video']"
/>
<p>Selected: {{ count($selectedMedia) }} files</p>
</div>
<form id="my-form" action="/posts" method="POST">
@csrf
{{-- Single file — populates a hidden input --}}
<livewire:file-picker input-name="featured_image" form-id="my-form" />
{{-- Multiple files — auto-submit form after selection --}}
<livewire:file-picker
input-name="gallery[]"
:multiple="true"
:max-files="10"
form-id="my-form"
:auto-submit="true"
/>
<button type="submit">Save</button>
</form>
<livewire:file-picker input-name="media" callback-function="onMediaSelected" />
<script>
function onMediaSelected(selected, inputName, inputId) {
console.log('Selected media:', selected);
}
</script>
| Property | Type | Default | Description |
|---|---|---|---|
multiple |
bool |
false |
Allow multiple selection |
maxFiles |
int |
10 |
Maximum number of files that can be selected |
selected |
array |
[] |
Pre-selected media IDs |
allowedTypes |
array |
[] |
Restrict to specific file types (empty = all) |
inputName |
string |
'files' |
Name for the hidden input(s) |
inputId |
string |
auto | ID for the hidden input |
formId |
string |
'' |
Form ID to target for auto-submit |
autoSubmit |
bool |
false |
Auto-submit the form after selection |
callbackFunction |
string |
'' |
Global JS function name called after selection |
buttonLabel |
string |
auto | Override the trigger button label |
showPreview |
bool |
true |
Show selected file previews below the button |
perPage |
int |
24 |
Items per page in the media library |
Restrict the picker via allowedTypes:
<livewire:file-picker :allowed-types="['image', 'document']" />
| Type | Extensions |
|---|---|
image |
jpg, jpeg, png, gif, webp, svg, bmp, ico, tiff, avif |
video |
mp4, webm, ogg, mov, avi, mkv, wmv, flv, m4v |
audio |
mp3, wav, aac, ogg, flac, m4a, wma, aiff |
document |
pdf, doc, docx, txt, rtf, odt, md, epub |
spreadsheet |
xls, xlsx, csv, ods, numbers |
presentation |
ppt, pptx, odp, key |
archive |
zip, rar, 7z, tar, gz, bz2, xz |
code |
js, ts, php, html, css, json, yaml, vue, jsx, tsx, py, go, rs, etc. |
Extensions per type can be customised in config/file-picker.php under extensions.
Fired on window after the user confirms a selection:
window.addEventListener('file-picker:selected', (event) => {
const { selected, inputName, inputId } = event.detail;
console.log('Selected media:', selected);
});
Each item in selected is an object with id, url, filename, size, extension, file_type, alt, created_at.
Two events fire whenever the selection changes — pick the one that fits your handler shape:
filesSelected — named arguments, easiest for typed signatures:
#[On('filesSelected')]
public function onFilesSelected(array $selected, string $inputName): void
{
// $selected is an array of media IDs (int)
// $inputName is the picker's `input-name` prop
$this->selectedIds = $selected;
}
file-picker-selected — single array payload with the full picker context:
#[On('file-picker-selected')]
public function onFilePickerSelected(array $payload): void
{
// keys: selected, inputName, inputId, formId, multiple, autoSubmit, callbackFunction
$this->selectedIds = $payload['selected'];
}
If you have several pickers on one page, switch on
$inputNameto route the selection to the right property.
Upload problems are surfaced to the UI at three levels:
{filename}: {message}).DuplicateMediaException, StorageQuotaExceededException, UploadFailedException, or any other Throwable thrown by the driver are aggregated into the toast (e.g. "2 uploaded, 1 failed").livewire-upload-error and forwards the HTTP status:
413 → "the file is larger than the server allows"422 → "the file did not pass validation"Error toasts are sticky — dismiss with the × button or any new upload action.
To push your own error into the toast (e.g. from a custom driver):
$this->setUploadError('Quota exceeded — contact your administrator.');
The library tab uses a side-by-side layout on desktop (≥1025px) with the Attachment Details panel always visible. On tablet/mobile (≤1024px), the panel becomes a right-side sheet that's closed by default — tapping a thumbnail just selects it.
To open the details sheet on touch devices, tap the edit icon that sits next to the selection checkbox on each item. It promotes the item to the active selection (without toggling existing selections off) and slides the sheet in. The icon is hidden on desktop where the sidebar is always inline. Rename it via the texts.view_details config key (or the published lang file).
Built on top of plank/laravel-mediable — installed automatically. The bundled FilePickerMedia model extends Plank's Media and the install migration adds the extra columns to Plank's existing media table.
FILE_PICKER_DRIVER=plank
FILE_PICKER_DISK=public
FILE_PICKER_DIRECTORY=media
Using a non-public disk? Plank's
mediable.allowed_disksconfig defaults to['public']. Publish Plank's config (php artisan vendor:publish --tag=mediable-config) and add your disk toallowed_disks.
Implement Anil\LivewireFilePicker\Contracts\MediaDriverInterface (or extend Anil\LivewireFilePicker\Drivers\AbstractDriver) and register the FQCN:
// config/file-picker.php
'driver' => \App\Media\MyCustomDriver::class,
Default is "everything allowed." For real apps, plug in an authorization class:
namespace App\Auth;
use Anil\LivewireFilePicker\Contracts\FilePickerAuthorizationInterface;
class MediaAuthorization implements FilePickerAuthorizationInterface
{
public function canViewLibrary(): bool { return auth()->check(); }
public function canUpload(): bool { return auth()->user()?->can('upload-media') ?? false; }
public function canDelete(int $mediaId): bool { return auth()->user()?->can('delete-media') ?? false; }
public function canEditAlt(int $mediaId): bool { return auth()->check(); }
}
// config/file-picker.php
'authorization_class' => \App\Auth\MediaAuthorization::class,
Add filter controls to the library toolbar in two parts:
1. UI controls in config:
'ui' => [
'custom_filters' => [
[
'name' => 'tag',
'label' => 'Tag',
'type' => 'select', // select | text | checkbox | date_range
'placeholder' => 'All Tags',
'options' => ['' => 'All Tags', 'nature' => 'Nature', 'urban' => 'Urban'],
],
[
'name' => 'featured',
'label' => 'Featured Only',
'type' => 'checkbox',
],
],
'custom_filter_class' => \App\Filters\MediaFilter::class,
],
2. The filter class:
namespace App\Filters;
use Anil\LivewireFilePicker\Contracts\CustomFilter;
use Illuminate\Database\Eloquent\Builder;
class MediaFilter implements CustomFilter
{
public function apply(Builder $query, array $filters): Builder
{
if (!empty($filters['tag'])) $query->where('tag', $filters['tag']);
if (!empty($filters['featured'])) $query->where('featured', true);
return $query;
}
}
Publish the config to customise everything:
php artisan vendor:publish --tag=file-picker-config
Key sections:
'driver' => env('FILE_PICKER_DRIVER', 'plank'), // 'plank' | CustomDriver::class
'drivers' => [
'plank' => [
'model' => FilePickerMedia::class,
'disk' => env('FILE_PICKER_DISK', 'public'),
'directory' => env('FILE_PICKER_DIRECTORY', 'media'),
'visibility' => env('FILE_PICKER_VISIBILITY', 'public'),
],
],
'max_file_size' => env('FILE_PICKER_MAX_SIZE', 102400), // KB (default: 100 MB)
'defaults' => [
'multiple' => false,
'max_files' => 40,
'per_page' => 24,
'show_preview' => true,
],
'sorting' => [
'field' => 'created_at', // created_at | filename | size | extension
'direction' => 'desc',
],
'features' => [
'upload' => true,
'delete' => true,
'bulk_delete' => true,
'edit_alt' => true,
'rename' => true,
'search' => true,
'filter' => true,
'sorting' => true,
'drag_drop' => true,
'refresh' => true,
'keyboard_navigation' => true,
'paste_upload' => true,
],
'ui' => [
'modal_style' => 'fullscreen', // 'fullscreen' | 'centered'
'thumbnail_height' => 150,
'show_type_badges' => true,
'show_file_size' => true,
'show_date' => true,
'colors' => [
'primary' => '#0073aa',
'primary_hover' => '#005a87',
'danger' => '#ef4444',
'success' => '#10b981',
'warning' => '#f59e0b',
],
'font_family' => "'Inter', sans-serif",
'border_radius' => 8,
'grid_min_width' => 160,
'grid_gap' => 14,
'sidebar_width' => 300,
'backdrop_blur' => 12,
'backdrop_opacity' => 0.6,
'z_index' => 9999,
'filter_types' => ['image', 'document', 'video', 'audio', 'spreadsheet', 'presentation'],
'custom_filters' => [],
'custom_filter_class' => '',
],
'route_middleware' => ['web'],
All UI text is configurable. Publish lang files to translate:
php artisan file-picker:install --lang
'texts' => [
'modal_title' => 'Media Library',
'tab_upload' => 'Upload Files',
'tab_library' => 'Media Library',
'drop_zone' => 'Drop files here or click to upload',
'search_placeholder' => 'Search media...',
'no_items' => 'No media found',
'insert_button' => 'Insert Selected',
'delete_confirm' => 'Are you sure you want to delete this file?',
'view_details' => 'View details', // tablet/mobile edit-icon label
'sidebar_title' => 'Attachment Details',
'close_details' => 'Close details',
// ... see config/file-picker.php for the full list
],
Publish blade views to override the UI:
php artisan file-picker:install --views
Views are published to resources/views/vendor/file-picker/.
| Method | Description |
|---|---|
openModal() / closeModal() |
Open / close the modal |
setViewMode('library'|'trash') |
Switch between active library and trash |
toggleSelection($id) |
Toggle selection of a media item |
viewDetails($id) |
Promote item to active and open details panel |
clearSelection() |
Clear all selected items |
insertSelected() |
Confirm selection and close modal |
uploadFiles() |
Upload pending files |
setUploadError($message) |
Push an error message into the upload toast |
deleteMedia($id) |
Soft-delete (move to trash) |
restoreMedia($id) |
Restore from trash |
forceDeleteMedia($id) |
Permanently delete (and remove file from disk) |
bulkDelete($ids) |
Soft-delete many at once |
toggleFavorite($id) |
Toggle favorite |
addTag() / removeTag($id, $tag) |
Manage tags |
startMoving($id) + saveMove() |
Move to a folder |
bulkMoveToFolder($ids, $folder) |
Move many at once |
startReplacing($id) |
Replace the underlying file |
refreshMedia() |
Reload media items |
clearFilters() |
Reset search / type / folder / tag / favorite |
use Anil\LivewireFilePicker\Facades\FilePicker;
FilePicker::upload($temporaryFile, ['folder' => 'reports', 'tags' => ['q1']]);
FilePicker::replaceFile($id, $newFile);
FilePicker::toggleFavorite($id);
FilePicker::addTag($id, 'archive-2024');
FilePicker::moveToFolder($id, 'archive/2024');
FilePicker::restore($id);
FilePicker::forceDelete($id);
FilePicker::getStats(); // counts, sizes, by_type, favorites_count, trashed_count
FilePicker::findByHash($sha256); // dedup lookups
php artisan file-picker:prune-trash --days=30 --dry-run
php artisan file-picker:prune-orphans --dry-run
php artisan file-picker:stats
| Route | Purpose |
|---|---|
GET /file-picker/download/{id} |
Force-download a single file |
GET /file-picker/download-zip?ids[]= |
Stream a zip of selected media |
| Property | Type | Description |
|---|---|---|
selectedMediaItems |
array |
Full details of selected media |
hasSelection |
bool |
Whether any items are selected |
selectionLabel |
string |
Human-readable selection count |
selectedCount |
int |
Number of selected items |
The MIT License (MIT). See License File.
How can I help you explore Laravel packages today?