solution-forest/filament-panzoom
A Filament plugin that adds smooth pan and zoom controls to images and media in your admin panel. Drag to pan, scroll or pinch to zoom, with configurable limits and responsive behavior. Drop it into any Filament resource or form to give users a closer look at uploaded files without leaving the page.
Install the package into an existing Filament app:
composer require solution-forest/filament-panzoom
php artisan filament:assets # or: php artisan vendor:publish --tag=filament-panzoom-assets
If the package ships a config file (config/filament-panzoom.php), publish it in the same step. There is no service provider registration to do manually — Filament auto-discovers the plugin.
Where to look first
src/ — the component class (typically a PanZoom view/form component). This tells you which chainable setters exist (imageUrl(), alt(), minZoom(), maxZoom(), disabled()).resources/views/ — the Blade template. Read this before overriding anything; it shows how the JS is bootstrapped and which CSS classes/attributes you can hook into.resources/js/ — the Alpine/vanilla wrapper around the underlying pan-zoom library. This is where the interaction options map to JS options.SolutionForest\FilamentPanzoom\... and a wrong namespace is the #1 first-run failure.First use case: a receipt/document viewer in a ViewRecord page. Drop the component into an Infolist on your OrderResource so support staff can inspect a scanned receipt full-screen without leaving the admin panel:
use SolutionForest\FilamentPanzoom\Infolists\PanZoomEntry; // confirm namespace in README
Infolists\Components\Section::make('Receipt')
->schema([
PanZoomEntry::make('receipt_url')
->imageUrl(fn ($record) => $record->getFirstMediaUrl('receipt'))
->maxZoom(5),
]),
Get one image rendering and zooming before you touch configuration.
Use it in an Infolist for read-only records. This is the natural fit — the component renders an image URL, it doesn't manage uploads. Pair it with Spatie Media Library or a plain Storage::url() accessor and let the entry's imageUrl() closure do the resolution.
Use it inside a table action modal. The highest-value pattern for "click a row, inspect the scan" workflows:
Tables\Actions\Action::make('viewReceipt')
->modalContent(fn ($record) => view('filament.partials.receipt-viewer', [
'url' => $record->receipt_url,
]))
->modalWidth('5xl')
Render the PanZoom component in that partial. Keeping the viewer in a modal avoids a full page navigation per inspection, which is where the zoom/pan actually earns its keep.
Use it as a read-only Form field when the image is part of an edit form alongside metadata fields (amount, date, vendor). Set ->disabled() or ->readOnly() so Filament doesn't try to hydrate a file state, and make sure the underlying column has no FileUpload counterpart competing for the same state path.
Feed it a signed/temporary URL, not a raw path. For private disks:
->imageUrl(fn ($record) => Storage::disk('private')->temporaryUrl(
$record->receipt_path, now()->addMinutes(10)
))
Blade usage for custom pages. If you're outside the schema system (custom Livewire page, dashboard widget), render the package view directly via the registered Blade namespace/component rather than reimplementing the JS. This keeps asset loading and Filament's theme integration intact.
Multiple images: there is generally no built-in gallery/carousel. Compose it yourself — either repeat the component inside a Tabs/Repeater-style layout, or use a Select that swaps the imageUrl() state. Don't expect the component to paginate documents for you.
Pair with ->columnSpanFull() in a Section. Cramming a pan-zoom viewer into a 3-column grid defeats the purpose — the viewport needs width.
php artisan filament:assets in your deploy script, or add it to the Composer post-autoload-dump hooks.wire:ignore, or ensure the component boots its JS on livewire:navigated / Alpine x-init rather than only on first DOMContentLoaded. This bites hardest inside forms where other fields trigger updates.wire:navigate / SPA mode. Confirm the JS re-initialises on navigation. If the viewer works on hard refresh but not after clicking through the panel, this is the cause.<img>, but if the underlying library ever draws to a canvas, a cross-origin S3 domain without CORS headers will blank or taint the canvas. Keep images on the app's own domain if you hit this.maxZoom(). Don't rely on the component to downsample.extraAttributes.content in tailwind.config.js for utility classes to survive compilation. Filament's default theme usually handles it; custom themes often don't.composer.json matches your Filament version (v3 vs v4 namespace and schema API differences are real). Pin the version in composer.json rather than floating ^.<img> src actually a valid URL (dump it in the Blade), (4) only then look at component options.--tag=filament-panzoom-views) and overriding the Blade template is the safe customisation path. Forking or patching vendor/ will not survive composer update.How can I help you explore Laravel packages today?