wezlo/filament-grid-list
Swap Filament resource tables for responsive grid/card lists without rewriting your resources. Define cards via sections, closures, or Blade views while keeping search, filters, sorting, pagination, bulk actions, and record actions working out of the box.
composer require wezlo/filament-grid-list
ListRecords page:
use Wezlo\FilamentGridList\Concerns\HasGridList;
class ListProducts extends ListRecords
{
use HasGridList;
// ...
}
gridList() in your page:
public function gridList(GridListConfiguration $config): GridListConfiguration
{
return $config
->header(fn ($record) => $record->name)
->content(fn ($record) => $record->description);
}
resources/css/filament/admin/theme.css):
@source '../../../../vendor/wezlo/filament-grid-list/resources/views/**/*';
Then rebuild assets (npm run build).Replace a table view for a ProductResource with a grid of product cards showing:
public function gridList(GridListConfiguration $config): GridListConfiguration
{
return $config
->image(fn ($record) => $record->thumbnail_url)
->header(fn ($record) => $record->name)
->content(fn ($record) => Str::limit($record->description, 100))
->footer(fn ($record) => '$' . number_format($record->price, 2));
}
Use structured sections for reusable, maintainable card layouts:
public function gridList(GridListConfiguration $config): GridListConfiguration
{
return $config
->image(fn ($record) => $record->image_url)
->header(fn ($record) => $record->title)
->badges(fn ($record) => [
['label' => $record->status->value, 'color' => $record->status->color],
])
->content(fn ($record) => view('components.product-summary', ['product' => $record]))
->footer(fn ($record) => $record->created_at->diffForHumans());
}
For dynamic HTML, use describeUsing():
$config->describeUsing(fn ($record) => new HtmlString(<<<HTML
<div class="p-4">
<h3 class="font-bold">{$record->name}</h3>
<div class="flex items-center gap-2 mt-2">
<span class="text-sm text-gray-500">{$record->category}</span>
<span class="badge badge-{$record->priority}">{$record->priority}</span>
</div>
</div>
HTML));
For complex layouts, create a dedicated Blade view (resources/views/products/grid-card.blade.php):
<div class="p-4">
<img src="{{ $record->image_url }}" class="w-full h-32 object-cover rounded">
<h3 class="mt-2 font-semibold">{{ $record->name }}</h3>
<p class="text-sm text-gray-600">{{ Str::limit($record->description, 80) }}</p>
<div class="mt-3 flex justify-between items-center">
<span class="font-bold">${{ number_format($record->price) }}</span>
<x-filament::badge color="info">{{ $record->stock }} in stock</x-filament::badge>
</div>
</div>
Then configure:
$config->cardView('products.grid-card');
Adjust columns per breakpoint:
$config->gridColumns([
'default' => 1, // Mobile: 1 column
'sm' => 2, // Small: 2 columns
'md' => 3, // Medium: 3 columns
'lg' => 4, // Large: 4 columns
'xl' => 6, // Extra large: 6 columns
]);
Ensure bulk actions work by:
table() method intact (filters, search, bulk actions are reused).$config->selectable(); // Enable bulk selection
Override default pagination:
$config->recordsPerPage(16)
->recordsPerPageOptions([8, 16, 32, 64]);
Set defaults for all grid lists in a panel:
FilamentGridListPlugin::make()
->gridColumns(['default' => 1, 'sm' => 2, 'md' => 3])
->gap(4)
->recordsPerPage(12)
->recordsPerPageOptions([12, 24, 48]);
The package includes dark mode classes (dark:*). Ensure your custom Blade views use:
<img src="{{ $record->image_url }}" class="dark:brightness-50">
Missing Tailwind Classes
@source to your Filament theme CSS and rebuild assets (npm run build).gap-4 not applied).Bulk Actions Not Working
table() method defines bulk actions (e.g., ->actions([...])).selectable() is called in gridList().table() method is not overridden to return null or an empty table.Pagination Conflicts
gridList() overrides the table() method. If using both, prioritize gridList():
$config->recordsPerPage(20); // Overrides table() defaults
Custom Blade Views Not Found
cardView() throws View [products.grid-card] not found.resources/views/ and the namespace is correct.Performance with Large Datasets
'all' in recordsPerPageOptions() (default excludes it).content-visibility: auto (enabled by default) for offscreen cards.->image(fn ($record) => $record->image_url, ['loading' => 'lazy'])
Record URL Overrides
recordUrl() if needed:
$config->recordUrl(fn ($record) => route('custom.route', $record));
Dark Mode Styling Issues
dark: variants for colors/backgrounds:
<div class="bg-white dark:bg-gray-800 p-4">
Inspect the Grid Configuration
Use dd($config) in gridList() to verify settings before rendering.
Check Livewire State
For bulk selection issues, inspect the filamentTable Alpine component in browser dev tools:
toggleSelectedRecord() and isRecordSelected() methods.Verify Table Infrastructure
Ensure getTableRecords(), getTableColumns(), and getTableFilters() are not overridden in a way that breaks the grid.
Tailwind Debugging
Add !important temporarily to test if a style is being overridden:
.fi-grid-list-card { gap: 1rem !important; }
Custom Card Components
Extend the grid by creating reusable components for sections (e.g., ProductCardHeader, ProductCardFooter) and include them in describeUsing().
Dynamic Grid Columns Use JavaScript to adjust columns based on user preference:
document.addEventListener('filament-grid-ready', () => {
const grid = document.querySelector('.fi-grid-list-content');
grid.style.gridTemplateColumns = 'repeat(auto-fit, minmax(250px, 1fr))';
});
Conditional Sections Show/hide sections based on record
How can I help you explore Laravel packages today?