archilex/filament-stacked-image-column
Filament Stacked Image Column displays multiple images as overlapping stacked thumbnails in Filament tables, with configurable overlap, sizing, and fallbacks. A lightweight, easy-to-install package for richer image columns in Laravel admin panels.
archilex/filament-stacked-image-column gives you a Filament table column that renders an array of image paths as an overlapping "avatar stack" instead of a single thumbnail. Day-to-day, it replaces writing a custom TextColumn with formatStateUsing() + HtmlString whenever a record has many images.
Install and register:
composer require archilex/filament-stacked-image-column
No service provider or config publishing is required — it's a column class you reference directly in a resource table:
use Archilex\StackedImageColumn\Columns\StackedImageColumn;
StackedImageColumn::make('photos')
->disk('public')
->size(32)
->circular()
->limit(4),
Look first at the package README for the exact method list for your installed version, then at src/Columns/StackedImageColumn.php — it's a thin subclass of Filament's ImageColumn, so everything you already know about ImageColumn (disk(), visibility(), size(), defaultImageUrl()) carries over and only the stacking behaviour is new.
The canonical first use case is a Spatie MediaLibrary collection or a JSON photos column on a model where you want a compact preview in the table and the full gallery on the View/Edit page. Start there, confirm it renders, then tune size/overlap/limit.
From a JSON array column. Cast the attribute in the model and point the column at it:
// Model
protected $casts = ['photos' => 'array'];
// Resource
StackedImageColumn::make('photos')->disk('public')->limit(5)
From a MediaLibrary collection. The column needs URLs or paths, not Media models, so map them and eager load to avoid N+1:
StackedImageColumn::make('media')
->getStateUsing(fn ($record) => $record->getMedia('gallery')->map->getUrl())
In the resource's getTableQuery() (or modifyQueryUsing), add ->with('media').
Mixed sources / computed stacks. Anything that resolves to an array of URLs works, so you can merge a primary image with fallbacks:
->getStateUsing(fn ($record) => collect([$record->cover_url])
->merge($record->photos ?? [])
->filter()
->values())
Placeholder handling (new in 1.1.0). defaultImageUrl() now works on the stacked column, so an empty array renders a placeholder rather than collapsing the cell:
StackedImageColumn::make('photos')
->defaultImageUrl(asset('images/placeholder.png'))
Pairing with the record view. A common pattern is a small stack in the table and the full set on the infolist/form — keep both driven by the same getStateUsing() closure or a model accessor so the two never drift.
Sizing consistency. Use the same size() value across your resource's avatar/image columns so table rows keep a uniform height; the stack's height is driven by the largest single image plus its ring.
defaultImageUrl() is column-wide, not per-item. It handles the empty-collection case; it will not backfill a single missing file inside an otherwise populated array. Filter or replace those entries in getStateUsing() yourself.'photos' => 'array' cast you'll get a string and a single broken image — a classic five-minute debug.getMedia() inside getStateUsing() runs per row; without with('media') a 50-row table issues 50 queries.overflow-hidden on the table cell or a custom row view can shear off rings and shadows. If the stack looks cut off, inspect the wrapping td before touching column options.limit() + remainder indicator. Setting a limit usually pairs with an "and N more" affordance; if you need the count, compute it in getStateUsing() or a separate column rather than expecting the column to expose it.ImageColumn, so a Filament v4 upgrade can shift inherited methods (size(), defaultImageUrl()) out from under it. Test the stack in a branch before bumping Filament.fetch-metadata) and has zero runtime impact — don't read it as a functional change.->getStateUsing(fn ($record) => tap($yourArray, fn ($v) => dump($v))); almost every visual bug is a wrong-shaped state, not a rendering bug.How can I help you explore Laravel packages today?