PHPackages                             wezlo/filament-grid-list - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. wezlo/filament-grid-list

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

wezlo/filament-grid-list
========================

Configurable grid/card layout that replaces the default Filament list table

1.0.0(1mo ago)2491↑50%1MITBladePHP ^8.2

Since Apr 12Pushed 1mo agoCompare

[ Source](https://github.com/mustafakhaleddev/filament-grid-list)[ Packagist](https://packagist.org/packages/wezlo/filament-grid-list)[ RSS](/packages/wezlo-filament-grid-list/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (1)Dependencies (2)Versions (2)Used By (0)

Filament Grid List
==================

[](#filament-grid-list)

Replace any Filament resource's default list table with a responsive card/grid layout. Define how each record card looks using structured sections, a closure, or a custom Blade view — while keeping all of Filament's search, filters, sorting, pagination, bulk actions, and record actions working out of the box.

No need to rewrite your resource. The same `table()` definition powers both the grid and the standard table.

Requirements
------------

[](#requirements)

- PHP 8.4+
- Laravel 13+
- Filament 4+

Features
--------

[](#features)

- **Responsive CSS grid** — configurable columns per Tailwind breakpoint (`default`, `sm`, `md`, `lg`, `xl`, `2xl`)
- **Three card rendering modes** — structured sections, closure-based, or custom Blade view
- **Full table infrastructure reuse** — search, filters, sorting, pagination, bulk selection, record actions, header actions, empty state all work without extra setup
- **Configurable pagination** — default records per page and page options at the page, plugin, or config level
- **Bulk selection** — checkboxes on each card, works with existing bulk actions
- **Record actions** — view, edit, delete, and custom actions render on each card
- **Click-to-navigate** — cards link to view/edit pages using the resource's existing record URL logic
- **Badge support** — render status badges with color on each card
- **Performance optimized** — `content-visibility: auto` skips rendering of offscreen cards, lazy-loaded images
- **Dark mode support**
- **Plugin-level defaults** — set grid columns, gap, and pagination globally for all grid list pages
- **Three-level configuration cascade** — page overrides plugin overrides config file

Installation
------------

[](#installation)

```
composer require wezlo/filament-grid-list
```

Optionally register the plugin in your Panel Provider for global defaults:

```
use Wezlo\FilamentGridList\FilamentGridListPlugin;

->plugins([
    FilamentGridListPlugin::make()
        ->gridColumns(['default' => 1, 'sm' => 2, 'lg' => 3])
        ->gap(4)
        ->recordsPerPage(12)
        ->recordsPerPageOptions([12, 24, 48, 96]),
])
```

Optionally publish the config:

```
php artisan vendor:publish --tag=filament-grid-list-config
```

### Theme Source (Tailwind v4)

[](#theme-source-tailwind-v4)

The package's Blade views use Tailwind utility classes. For Tailwind to detect them during your app's build, add the package's views as a `@source` in your Filament custom theme CSS file (usually `resources/css/filament/admin/theme.css`):

```
@import '../../../../vendor/filament/filament/resources/css/theme.css';

@source '../../../../vendor/wezlo/filament-grid-list/resources/views/**/*';

@custom-variant dark (&:where(.dark, .dark *));
```

If you don't have a custom theme yet, create one:

```
php artisan make:filament-theme
```

Then rebuild assets:

```
npm run build
```

Without this step, some Tailwind utilities used in the grid view may be missing from the compiled CSS.

Quick Start
-----------

[](#quick-start)

Add the `HasGridList` trait to your resource's `ListRecords` page and implement the `gridList()` method:

```
use Filament\Resources\Pages\ListRecords;
use Wezlo\FilamentGridList\Concerns\HasGridList;
use Wezlo\FilamentGridList\GridListConfiguration;

class ListProducts extends ListRecords
{
    use HasGridList;

    protected static string $resource = ProductResource::class;

    public function gridList(GridListConfiguration $config): GridListConfiguration
    {
        return $config
            ->gridColumns(['default' => 1, 'sm' => 2, 'lg' => 4])
            ->header(fn ($record) => $record->name)
            ->content(fn ($record) => Str::limit($record->description, 100))
            ->footer(fn ($record) => '$' . number_format($record->price, 2));
    }
}
```

That's it. The list page now renders as a card grid. Your existing `table()` definition continues to power filters, search, actions, and pagination.

Card Rendering Modes
--------------------

[](#card-rendering-modes)

### Mode 1: Structured Sections

[](#mode-1-structured-sections)

Build cards from discrete sections. Each receives the Eloquent record and returns a string or `HtmlString`. All sections are optional — use any combination.

```
public function gridList(GridListConfiguration $config): GridListConfiguration
{
    return $config
        ->image(fn ($record) => $record->thumbnail_url)
        ->header(fn ($record) => $record->name)
        ->badges(fn ($record) => [
            ['label' => $record->status->getLabel(), 'color' => $record->status->getColor()],
            ['label' => $record->category->name, 'color' => 'info'],
        ])
        ->content(fn ($record) => $record->short_description)
        ->footer(fn ($record) => '$' . number_format($record->price, 2));
}
```

SectionClosure SignatureDescription`image(Closure)``fn (Model $record): ?string`Hero image URL at the top of the card`header(Closure)``fn (Model $record): ?string`Card title text`badges(Closure)``fn (Model $record): array`Array of badge definitions (see below)`content(Closure)``fn (Model $record): string|HtmlString|null`Card body text or HTML`footer(Closure)``fn (Model $record): ?string`Bottom section (price, date, metadata)#### Badge Format

[](#badge-format)

Badges can be either arrays or renderable Blade components:

```
// Array format (recommended)
['label' => 'Active', 'color' => 'success']

// Any renderable (Blade component, HtmlString, etc.)
view('components.my-badge', ['status' => $record->status])
```

Available badge colors: `primary`, `secondary`, `success`, `danger`, `warning`, `info`, `gray`.

### Mode 2: Closure-based

[](#mode-2-closure-based)

Return raw HTML for full control over the card body:

```
use Illuminate\Support\HtmlString;

$config->describeUsing(fn ($record) => new HtmlString("

        {$record->name}
        {$record->description}
        \${$record->price}

"));
```

### Mode 3: Custom Blade View

[](#mode-3-custom-blade-view)

Point to your own Blade view for maximum flexibility:

```
$config->cardView('products.grid-card');
```

The view receives three variables:

VariableTypeDescription`$record``Model`The Eloquent record`$recordKey``string`The record's primary key`$recordUrl``?string`URL the card links to (null if no link)```
{{-- resources/views/products/grid-card.blade.php --}}

    {{ $record->name }}
    {{ $record->description }}

        ${{ number_format($record->price, 2) }}
        {{ $record->created_at->diffForHumans() }}

```

### Rendering Priority

[](#rendering-priority)

When multiple modes are configured, the first match wins:

1. `cardView()` (custom Blade view)
2. `describeUsing()` (closure)
3. Structured sections (`header()`, `content()`, etc.)
4. Fallback: displays the record's title via `getTableRecordTitle()`

Configuration Reference
-----------------------

[](#configuration-reference)

### Grid Layout

[](#grid-layout)

```
$config->gridColumns(['default' => 1, 'sm' => 2, 'md' => 3, 'lg' => 4])
       ->gap(6);  // Tailwind spacing unit (6 = 1.5rem)
```

Breakpoint keys follow Tailwind: `default`, `sm`, `md`, `lg`, `xl`, `2xl`. The value is the number of columns at that breakpoint.

### Pagination

[](#pagination)

Set pagination on the grid config (overrides the table's defaults):

```
$config->recordsPerPage(24)
       ->recordsPerPageOptions([12, 24, 48, 96]);
```

Or configure it on your `table()` method as usual — the grid respects it:

```
public function table(Table $table): Table
{
    return $table
        ->columns([...])
        ->defaultPaginationPageOption(12)
        ->paginationPageOptions([12, 24, 48, 96]);
}
```

All Filament pagination modes (Default, Simple, Cursor) work.

> **Note:** The `'all'` page option is excluded from defaults to prevent page hangs on large datasets. Cards use `content-visibility: auto` for offscreen rendering optimization, but loading thousands of DOM nodes can still be slow. If you need it, opt in explicitly: `->recordsPerPageOptions([12, 24, 48, 'all'])`.

### Bulk Selection

[](#bulk-selection)

Enabled by default when the table has bulk actions. Disable per-page with:

```
$config->selectable(false);
```

### Record Click URL

[](#record-click-url)

By default, cards link to the view/edit page using the resource's existing record URL logic (same as clicking a table row). Override per-page with:

```
$config->recordUrl(fn ($record) => route('products.show', $record));
```

Plugin Configuration
--------------------

[](#plugin-configuration)

Register the plugin in your Panel Provider to set defaults for all grid list pages in that panel:

```
use Wezlo\FilamentGridList\FilamentGridListPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugins([
            FilamentGridListPlugin::make()
                ->gridColumns(['default' => 1, 'sm' => 2, 'md' => 3, 'lg' => 4])
                ->gap(4)
                ->recordsPerPage(12)
                ->recordsPerPageOptions([12, 24, 48, 96]),
        ]);
}
```

MethodTypeDefaultDescription`gridColumns(array)``array``['default' => 1, 'sm' => 2, 'md' => 3, 'lg' => 4]`Responsive grid columns`gap(int)``int``4`Tailwind spacing unit for card gap`recordsPerPage(int)``int``12`Default records per page`recordsPerPageOptions(array)``array``[12, 24, 48, 96]`Per-page dropdown optionsConfiguration Cascade
---------------------

[](#configuration-cascade)

Each setting resolves through a three-level cascade:

1. **Page-level** — values set in `gridList()` on the `ListRecords` page (highest priority)
2. **Plugin-level** — values set on `FilamentGridListPlugin` in the Panel Provider
3. **Config file** — values in `config/filament-grid-list.php` (lowest priority)

Page-level always wins. If not set, falls through to plugin, then config.

Default Config File
-------------------

[](#default-config-file)

```
// config/filament-grid-list.php
return [
    'grid_columns' => [
        'default' => 1,
        'sm' => 2,
        'md' => 3,
        'lg' => 4,
    ],
    'gap' => 4,
    'records_per_page' => 12,
    'records_per_page_options' => [12, 24, 48, 96],
];
```

Full Example
------------

[](#full-example)

```
use App\Enums\ProductStatus;
use Filament\Resources\Pages\ListRecords;
use Illuminate\Support\Str;
use Wezlo\FilamentGridList\Concerns\HasGridList;
use Wezlo\FilamentGridList\GridListConfiguration;

class ListProducts extends ListRecords
{
    use HasGridList;

    protected static string $resource = ProductResource::class;

    public function gridList(GridListConfiguration $config): GridListConfiguration
    {
        return $config
            ->gridColumns(['default' => 1, 'sm' => 2, 'md' => 3, 'xl' => 4])
            ->gap(6)
            ->recordsPerPage(24)
            ->recordsPerPageOptions([12, 24, 48])
            ->image(fn ($record) => $record->thumbnail_url)
            ->header(fn ($record) => $record->name)
            ->badges(fn ($record) => [
                ['label' => $record->status->getLabel(), 'color' => $record->status->getColor()],
                ['label' => $record->category->name, 'color' => 'info'],
            ])
            ->content(fn ($record) => Str::limit($record->description, 120))
            ->footer(fn ($record) => '$' . number_format($record->price, 2))
            ->selectable()
            ->recordUrl(fn ($record) => ProductResource::getUrl('view', ['record' => $record]));
    }
}
```

The resource's `table()` method stays unchanged — columns, filters, search, actions, and bulk actions all carry over to the grid view automatically.

How It Works
------------

[](#how-it-works)

- The `HasGridList` trait overrides `content()` on the `ListRecords` page to render a custom Blade view instead of the default `EmbeddedTable`
- The trait overrides `makeTable()` to apply pagination config from `GridListConfiguration`
- Records come from `$this->getTableRecords()`, which handles the full filtered/sorted/paginated query pipeline from Filament's `InteractsWithTable` trait
- The view initializes Filament's `filamentTable()` Alpine component for selection state management
- Search binds to `wire:model.live.debounce` on the existing `tableSearch` Livewire property
- Filters render using the table's filter trigger action and `$this->getTableFiltersForm()`
- Bulk selection uses `toggleSelectedRecord()` / `isRecordSelected()` from the `filamentTable` Alpine component — same API as the standard table
- Record actions are cloned per-record using `$action->getClone()` (same pattern as the standard table view)
- Pagination renders via `` with the paginator from `getTableRecords()`
- Cards use `content-visibility: auto` CSS so the browser skips layout/paint for offscreen cards

Performance
-----------

[](#performance)

- **`content-visibility: auto`** — offscreen cards skip layout and paint, keeping the page responsive even with many records
- **Lazy-loaded images** — card images use `loading="lazy"` so only visible images are fetched
- **No `'all'` by default** — pagination defaults to `[12, 24, 48, 96]` to prevent DOM overload. Opt in to `'all'` explicitly if needed
- **Deferred loading support** — respects Filament's `wire:init="loadTable"` for deferred table loading

CSS Classes
-----------

[](#css-classes)

All card elements use `fi-grid-list-*` prefixed classes for targeted styling:

ClassElement`fi-grid-list`Root container`fi-grid-list-content`Grid container`fi-grid-list-card`Individual card`fi-grid-list-card-clickable`Card with a link`fi-grid-list-card-checkbox`Selection checkbox wrapper`fi-grid-list-card-body`Card content wrapper`fi-grid-list-card-image`Image container`fi-grid-list-card-img`Image element`fi-grid-list-card-content`Text content area`fi-grid-list-card-title`Header/title text`fi-grid-list-card-badges`Badges container`fi-grid-list-card-description`Content/description text`fi-grid-list-card-footer`Footer area`fi-grid-list-card-actions`Record actions barOverride any of these in your theme CSS to customize the card appearance.

License
-------

[](#license)

MIT

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance88

Actively maintained with recent releases

Popularity22

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

58d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/25182746?v=4)[Mustafa Khaled](/maintainers/mustafakhaleddev)[@mustafakhaleddev](https://github.com/mustafakhaleddev)

---

Top Contributors

[![mustafakhaleddev](https://avatars.githubusercontent.com/u/25182746?v=4)](https://github.com/mustafakhaleddev "mustafakhaleddev (2 commits)")

---

Tags

laravelgridlistfilamentcards

### Embed Badge

![Health badge](/badges/wezlo-filament-grid-list/health.svg)

```
[![Health](https://phpackages.com/badges/wezlo-filament-grid-list/health.svg)](https://phpackages.com/packages/wezlo-filament-grid-list)
```

###  Alternatives

[ysfkaya/filament-phone-input

A phone input component for Laravel Filament

3131.2M25](/packages/ysfkaya-filament-phone-input)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3913.7k](/packages/rawilk-profile-filament-plugin)[dotswan/filament-map-picker

Easily pick and retrieve geo-coordinates using a map-based interface in your Filament applications.

127173.7k3](/packages/dotswan-filament-map-picker)[stephenjude/filament-feature-flags

Filament implementation of feature flags and segmentation with Laravel Pennant.

122157.7k1](/packages/stephenjude-filament-feature-flags)[creagia/filament-code-field

A Filamentphp input field to edit or view code data.

57301.3k3](/packages/creagia-filament-code-field)[jibaymcs/filament-tour

Bring the power of DriverJs to your Filament panels and start a tour !

12351.0k](/packages/jibaymcs-filament-tour)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
