anil/file-picker

A WordPress-like file picker component for Laravel Livewire. Supports images, videos, audio, documents and all file types with a beautiful modal interface.

Maintainers

Package info

github.com/anilkumarthakur60/livewire-file-picker

pkg:composer/anil/file-picker

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-05-28 17:14 UTC

This package is auto-updated.

Last update: 2026-05-28 17:29:53 UTC


README

A WordPress-like file picker component for Laravel Livewire. Supports images, videos, audio, documents, and all file types with a beautiful modal interface.

Features

  • 📁 All File Types — Images, videos, audio, documents, spreadsheets, presentations, archives, and code files
  • 🎨 Beautiful UI — Modern, clean interface inspired by WordPress media library, with a responsive Sheet-style detail panel on tablet/mobile
  • 🔍 Search & Filter — Quick search and filter by file type, folder, tag, or favorite
  • 📤 Drag & Drop + Paste — Drag-drop files in, or paste from clipboard
  • 🚨 Upload Error Reporting — Validation errors render per-file, browser-side failures (413 / network) surface in the same toast
  • Single / Multiple Selection — Configurable max-files
  • 🗑️ Trash & Restore — Soft-delete with recoverable trash + retention-based pruning
  • 🔁 Replace File — Update an existing media record's file in place
  • 🪪 Hash & Duplicate Detection — SHA-256 dedup with reuse, reject, or allow strategies
  • Favorites — Star important items
  • 🏷️ Tags — Free-form labels for richer organization
  • 📂 Folders — Group media into folders with single & bulk move
  • ✏️ Inline Editing — Rename and edit alt text without leaving the picker
  • 👤 Ownership Tracking — Auto-record user_id, optionally scope library per user
  • 📊 Storage Quotas — Global and per-user storage caps
  • 📈 Statistics API — Aggregate counts / sizes / by-type via FilePicker::getStats()
  • 📥 Downloads — Force-download single files or bulk download as ZIP
  • 🛠️ Console Commandsfile-picker:prune-trash, file-picker:prune-orphans, file-picker:stats
  • ⚙️ Highly Configurable — Feature toggles, theme colors, custom drivers, custom authorization, custom filters
  • 🎯 Form Integration — Works with Livewire components and traditional HTML forms
  • Accessible — Keyboard navigation, focus management, Esc-to-close

Requirements

  • PHP 8.2+ (also tested on 8.3 / 8.4)
  • Laravel 11.x, 12.x, or 13.x
  • Livewire 3.x or 4.x
  • plank/laravel-mediable ^6.0 — installed automatically

Installation

1. Install via Composer

composer require anil/file-picker

2. Run the install command

php artisan file-picker:install

This single command will:

  • Publish config/file-picker.php
  • Run the package migration — an additive migration that adds the package's columns (folder, tags, is_favorite, hash, etc.) to Plank's media table

That's it. Assets (CSS/JS) are served automatically via a built-in route — no need to publish them.

Optional flags

# Overwrite already-published files
php artisan file-picker:install --force

# Skip running migrations (publish only)
php artisan file-picker:install --no-migrate

# Also publish blade views for UI customisation
php artisan file-picker:install --views

# Also publish language files to override text strings
php artisan file-picker:install --lang

# Publish CSS/JS to public/ (not required — served via route by default)
php artisan file-picker:install --assets

3. Add stack slots to your layout

The component pushes its CSS into @stack('head') and its JS into @stack('scripts'). Add these to your layout if not already present:

<!DOCTYPE html>
<html>
<head>
    ...
    @stack('head')
</head>
<body>
    ...
    {{ $slot }}

    @stack('scripts')
</body>
</html>

Usage

Basic usage

{{-- Single file selection --}}
<livewire:file-picker input-name="featured_image" />

{{-- Multiple file selection --}}
<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']" />

In a Livewire component

namespace App\Livewire;

use Livewire\Component;
use Livewire\Attributes\On;

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('livewire.post-form');
    }
}
{{-- resources/views/livewire/post-form.blade.php --}}
<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>

In a traditional HTML form

<form id="my-form" action="/posts" method="POST">
    @csrf

    {{-- Single file — populates a hidden input on selection --}}
    <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>

With a JavaScript callback

<livewire:file-picker
    input-name="media"
    callback-function="onMediaSelected"
/>

<script>
function onMediaSelected(selected, inputName, inputId) {
    console.log('Selected media:', selected);
}
</script>

Component Properties

Property Type Default Description
multiple bool false Allow multiple file 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 attribute for the hidden input(s)
inputId string auto ID attribute 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

Allowed File Types

Restrict the picker to one or more types 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.

You can customize extensions per type in config/file-picker.php under extensions.

Events

JavaScript event

Fired on the window after the user confirms their 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.

Livewire events

Two Livewire events fire every time 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 (route by it if you have multiple pickers on one page)
    $this->selectedIds = $selected;
}

file-picker-selected — single array payload with the full picker context (used by the bundled JS callback support):

#[On('file-picker-selected')]
public function onFilePickerSelected(array $payload): void
{
    // $payload keys: selected, inputName, inputId, formId, multiple, autoSubmit, callbackFunction
    $this->selectedIds = $payload['selected'];
}

If you have several pickers on the same page, switch on $inputName (or $payload['inputName']) to route the selection to the right property.

Upload Errors

Upload problems are surfaced to the UI on three levels:

  1. Server-side validation (size, mime type) — uploadFiles() runs the configured mimes / max rules and any failures are rendered:
    • as a toast at the top of the upload tab (uploadStatus = 'error'), and
    • as a per-file list ({original_filename}: {message}) below the toast.
  2. Per-file driver failures during processingDuplicateMediaException, StorageQuotaExceededException, UploadFailedException, and any other Throwable thrown by the driver are caught and aggregated into the same toast (e.g. "2 uploaded, 1 failed").
  3. Browser-side failures — the bundled JS listens for livewire-upload-error on the picker's file input and forwards the HTTP status to the component via setUploadError(string $message). Common cases are mapped automatically:
    • 413"the file is larger than the server allows"
    • 422"the file did not pass validation"
    • other status codes → generic "Upload failed (HTTP …)"

Error toasts are sticky — they don't auto-clear like success messages do. The user dismisses them with the × button (or any new upload action resets the state).

If you want to push your own error into the toast from a parent component or hook, call setUploadError:

$this->dispatch('refresh-file-picker'); // example
// or, from inside your custom driver / extension:
$this->setUploadError('Quota exceeded — contact your administrator.');

Tablet & Mobile

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 collapsed by default — tapping an item just selects it.

To open the details sheet on touch devices, an edit icon appears on each grid item (top-left, right after the selection checkbox). Tapping it:

  • promotes the item to the active selection (without toggling existing selections off),
  • dispatches open-details, which slides the sheet in.

The icon is hidden on desktop where the sidebar is always inline. You can rename the button via the texts.view_details config key (or the published lang file).

Drivers

Plank driver (default)

The package is built on top of plank/laravel-mediable — installed automatically as a hard dependency. The bundled FilePickerMedia model extends Plank's Media model and the install migration adds the extra columns we need (folder, tags, is_favorite, hash, width, height, duration, user_id, download_count, custom_properties, deleted_at) to Plank's media table.

Defaults:

FILE_PICKER_DRIVER=plank
FILE_PICKER_DISK=public
FILE_PICKER_DIRECTORY=media

Using a non-public disk? Plank's mediable.allowed_disks config defaults to ['public'] only. To use s3 or another disk, publish Plank's config (php artisan vendor:publish --tag=mediable-config) and add your disk to allowed_disks.

Custom driver

Implement Anil\LivewireFilePicker\Contracts\MediaDriverInterface (or extend Anil\LivewireFilePicker\Drivers\AbstractDriver) and register the FQCN as the driver:

// config/file-picker.php
'driver' => \App\Media\MyCustomDriver::class,

Authorization

By default all actions are permitted. To add authorization, implement FilePickerAuthorizationInterface:

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();
    }
}

Register it in the config:

// config/file-picker.php
'authorization_class' => \App\Auth\MediaAuthorization::class,

Custom Filters

Add custom filter controls to the media library toolbar. Two parts are required:

1. Define the UI controls in config:

// config/file-picker.php
'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. Implement 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;
    }
}

Configuration Reference

Publish the config to customize everything:

php artisan vendor:publish --tag=file-picker-config

Driver

'driver' => env('FILE_PICKER_DRIVER', 'plank'), // 'plank' | CustomDriver::class

'drivers' => [
    'plank' => [
        'model'      => FilePickerMedia::class, // extends Plank\Mediable\Media
        'disk'       => env('FILE_PICKER_DISK', 'public'),
        'directory'  => env('FILE_PICKER_DIRECTORY', 'media'),
        'visibility' => env('FILE_PICKER_VISIBILITY', 'public'),
    ],
],

Upload limits

'max_file_size' => env('FILE_PICKER_MAX_SIZE', 102400), // KB (default: 100 MB)

Defaults

'defaults' => [
    'multiple'     => false,
    'max_files'    => 40,
    'per_page'     => 24,
    'show_preview' => true,
],

Sorting

'sorting' => [
    'field'     => 'created_at', // created_at | filename | size | extension
    'direction' => 'desc',       // asc | desc
],

Feature toggles

'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 / Theme

'ui' => [
    'modal_style'           => 'fullscreen',   // 'fullscreen' | 'centered'
    'thumbnail_height'      => 150,            // px
    '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,              // px
    'grid_min_width'        => 160,            // px
    'grid_gap'              => 14,             // px
    'sidebar_width'         => 300,            // px
    'backdrop_blur'         => 12,             // px
    'backdrop_opacity'      => 0.6,
    'z_index'               => 9999,
    'upload_preview_size'   => 120,            // px
    'upload_area_max_height'=> 400,            // px

    'filter_types' => ['image', 'document', 'video', 'audio', 'spreadsheet', 'presentation'],

    'custom_filters'      => [],
    'custom_filter_class' => '',
],

Text strings

All text in the UI is configurable and can also be translated via published lang files:

php artisan file-picker:install --lang
# or
php artisan vendor:publish --tag=file-picker-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',     // label for the tablet/mobile edit icon
    'sidebar_title'      => 'Attachment Details',
    'close_details'      => 'Close details',
    // ... see config/file-picker.php for the full list
],

Route middleware

'route_middleware' => ['web'],

The package registers a route at /vendor/anil/livewire-file-picker/{file} to serve CSS/JS assets. This route is protected by the middleware listed here.

Customising Views

Publish the blade views to override the UI:

php artisan file-picker:install --views
# or
php artisan vendor:publish --tag=file-picker-views

Views will be published to resources/views/vendor/file-picker/.

API Reference

FilePicker Component Methods

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 the details panel (used by the tablet/mobile edit icon)
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 (also called from the JS upload-error hook)
deleteMedia($id) Soft-delete a media item (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 on a media item
startMoving($id) + saveMove() Move a media item to a folder
bulkMoveToFolder($ids, $folder) Move many at once
startReplacing($id) Replace the underlying file (next upload swaps it)
refreshMedia() Reload media items
clearFilters() Reset search/type/folder/tag/favorite filters

FilePicker Facade

Outside the component, drive the library directly:

use Anil\LivewireFilePicker\Facades\FilePicker;

FilePicker::upload($temporaryFile, ['folder' => 'reports', 'tags' => ['q1', 'finance']]);
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

Console Commands

php artisan file-picker:prune-trash --days=30 --dry-run
php artisan file-picker:prune-orphans --dry-run
php artisan file-picker:stats

Download Routes

Route Purpose
GET /file-picker/download/{id} Force-download a single file
GET /file-picker/download-zip?ids[]= Stream a zip of selected media

Computed Properties

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

Versioning

This package follows Semantic Versioning. While we're in the 0.x line the public API may change between minor releases — pin to a specific 0.x range (e.g. ^0.1) rather than across them.

composer require anil/file-picker:^0.1

Note on ^0.1: Composer expands this to >=0.1.0, <0.2.0. When 0.2.0 lands you'll need to bump the constraint deliberately (which is the intended behaviour for pre-1.0 packages).

Static Analysis

This package uses PHPStan with Larastan at level 8.

composer analyse

Contributing

Contributions are welcome! Please open an issue or pull request.

License

The MIT License (MIT). Please see License File for more information.

Credits