wezlo / filament-responsive-table
Render a Filament list table as stacked cards below a configurable Tailwind breakpoint.
Package info
github.com/mustafakhaleddev/filament-responsive-table
pkg:composer/wezlo/filament-responsive-table
Requires
- php: ^8.2
- filament/filament: ^4.0 || ^5.0
- spatie/laravel-package-tools: ^1.0
This package is auto-updated.
Last update: 2026-04-22 01:13:26 UTC
README
Render a Filament list table as stacked cards below a configurable Tailwind breakpoint. Above the breakpoint nothing changes — the native Filament table renders untouched. Below the breakpoint each row becomes a card with column labels, values, and the row's record actions in the footer.
The same table() definition drives both views — no duplicate column lists, no second source of truth.
Requirements
- PHP 8.2+
- Laravel 11+ / 13
- Filament 4 or 5
Features
- Breakpoint-driven — pick
sm,md,lg,xl, or2xl; below it the table becomes cards, above it the native table renders - Zero duplication — columns and record actions come straight from your existing
table()method - Two-column label/value grid inside each card, matching mobile UX expectations
- Optional per-card title resolved from the record
- Optional bulk-selection checkbox on each card so bulk actions still work on mobile
- Optional record actions footer — stack the row's actions at the bottom of each card
- Column filtering —
only()/except()hide columns in cards without affecting the desktop table - Custom card Blade view — opt out of the default template whenever you need it
- Plugin-level defaults — set a single default breakpoint for every responsive list page in a panel
- Three-level configuration cascade — page overrides plugin overrides config file
- Dark mode support
Installation
composer require wezlo/filament-responsive-table
Optionally register the plugin in your Panel Provider for global defaults:
use Wezlo\FilamentResponsiveTable\FilamentResponsiveTablePlugin; ->plugins([ FilamentResponsiveTablePlugin::make() ->defaultBreakpoint('md'), ])
Optionally publish the config:
php artisan vendor:publish --tag=filament-responsive-table-config
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-responsive-table/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
Quick Start
Add the HasResponsiveTable trait to your resource's ListRecords page and declare a $responsiveBreakpoint property to pick the breakpoint:
use Filament\Resources\Pages\ListRecords; use Wezlo\FilamentResponsiveTable\Concerns\HasResponsiveTable; class ListUsers extends ListRecords { use HasResponsiveTable; protected static string $resource = UserResource::class; public ?string $responsiveBreakpoint = 'md'; }
That's it. At viewports < md (768px) every row collapses into a card showing each column's label on the left and its rendered value on the right. Record actions stack in a footer below.
The trait does not declare
$responsiveBreakpointitself (to avoid PHP trait/class property conflicts), so you declare it on your list page with whatever visibility and default you like —public,protected,?string, or a non-nullablestringwith a default value all work.
Configuration API
For anything beyond the breakpoint shortcut, implement the responsiveTable() method. The configuration from the method wins over the $responsiveBreakpoint property.
use Wezlo\FilamentResponsiveTable\Concerns\HasResponsiveTable; use Wezlo\FilamentResponsiveTable\ResponsiveTableConfiguration; class ListUsers extends ListRecords { use HasResponsiveTable; public function responsiveTable(ResponsiveTableConfiguration $config): ResponsiveTableConfiguration { return $config ->breakpoint('lg') ->except(['id', 'created_at']) ->cardTitle(fn ($record) => $record->name) ->showRecordActions() ->showBulkSelection(); } }
Configuration Reference
| Method | Signature | Description |
|---|---|---|
breakpoint(string) |
'sm' | 'md' | 'lg' | 'xl' | '2xl' |
Below this Tailwind breakpoint, rows render as cards. Throws on unknown values. |
only(array) |
array<string> |
Keep only these column names in cards. Desktop table is untouched. |
except(array) |
array<string> |
Hide these column names from cards. Desktop table is untouched. |
cardTitle(Closure) |
fn (Model $record): string|Htmlable|null |
Resolve a per-card header title from the record. |
showRecordActions(bool) |
bool (default true) |
Render the table's record actions in each card's footer. |
showBulkSelection(bool) |
bool (default false) |
Show the bulk-selection checkbox on each card. |
cardView(string) |
string |
Override the default card Blade template. |
Column Filtering
only() and except() match against Column::getName() — the first argument to TextColumn::make('name'), IconColumn::make('status'), etc. Nested relationship columns like client.name match by that exact name.
$config->only(['name', 'email', 'status']); // or $config->except(['id', 'created_at', 'updated_at', 'deleted_at']);
Card Title
Without cardTitle(), the card has no header bar — it's just label/value rows and the action footer. Setting it renders a header strip with the title text (and the bulk checkbox if enabled).
$config->cardTitle(fn ($record) => "#{$record->invoice_number}"); $config->cardTitle(fn ($record) => new HtmlString("<strong>{$record->name}</strong>"));
Custom Card View
Point to your own Blade view if the default layout doesn't fit:
$config->cardView('users.mobile-card');
The view receives three variables:
| Variable | Type | Description |
|---|---|---|
$record |
Model |
The Eloquent record for this card |
$columns |
array<Column> |
The visible card columns (after only/except) |
$config |
ResponsiveTableConfiguration |
The resolved configuration |
You can use $this->getResponsiveTableRecordActions($record) inside the view to get the cloned, record-bound, visibility-filtered actions array.
Plugin Configuration
Register the plugin in your Panel Provider to set defaults for all responsive list pages in that panel:
use Wezlo\FilamentResponsiveTable\FilamentResponsiveTablePlugin; public function panel(Panel $panel): Panel { return $panel ->plugins([ FilamentResponsiveTablePlugin::make() ->defaultBreakpoint('md') ->defaultShowRecordActions(true) ->defaultShowBulkSelection(false), ]); }
| Method | Type | Default | Description |
|---|---|---|---|
defaultBreakpoint(string) |
string |
null |
Default breakpoint when no page sets one |
defaultShowRecordActions(bool) |
bool |
null |
Default visibility of the actions footer |
defaultShowBulkSelection(bool) |
bool |
null |
Default visibility of the bulk checkbox |
Configuration Cascade
Each setting resolves through a four-level cascade:
responsiveTable()method on theListRecordspage (highest priority)$responsiveBreakpointproperty on theListRecordspage (breakpoint only)- Plugin defaults on
FilamentResponsiveTablePluginin the Panel Provider - Config file —
config/filament-responsive-table.php(lowest priority)
The method always wins over the property, which wins over plugin defaults, which win over the config file.
Default Config File
// config/filament-responsive-table.php return [ 'breakpoint' => 'md', 'show_record_actions' => true, 'show_bulk_selection' => false, ];
Full Example
use Filament\Resources\Pages\ListRecords; use Wezlo\FilamentResponsiveTable\Concerns\HasResponsiveTable; use Wezlo\FilamentResponsiveTable\ResponsiveTableConfiguration; class ListOrders extends ListRecords { use HasResponsiveTable; protected static string $resource = OrderResource::class; public function responsiveTable(ResponsiveTableConfiguration $config): ResponsiveTableConfiguration { return $config ->breakpoint('lg') ->except(['id']) ->cardTitle(fn ($record) => "#{$record->number}") ->showRecordActions() ->showBulkSelection(); } }
The resource's table() method stays unchanged — columns, filters, search, header actions, record actions, and bulk actions all carry over to the card view automatically.
How It Works
- The
HasResponsiveTabletrait overridescontent()on theListRecordspage to render a single wrapper view that contains both the native Filament table (via$this->getTable()->render()) and a card stack generated from the same columns. - The wrapper
<div>carriesdata-breakpoint="<bp>". A small shipped stylesheet has static@mediarules — at the configured breakpoint it hides the table and shows the cards, and vice-versa above it. Because the rules are static CSS (not Tailwind utilities), Tailwind's JIT scan isn't required for visibility toggling. - Each card pulls its columns from
$this->getTable()->getVisibleColumns(), then appliesonly/except. Columns are cloned per record ($column->getClone()->record($record)) so the same render pipeline used by the desktop table — badges, icons, date formatting, images — produces the card values. - Record actions are cloned per record (
$action->getClone()->record($record)) and filtered byisHidden(), mirroring the pattern in Filament's own table Blade view. - The desktop table is Filament's native
Table::render()output — search, filters, sorting, pagination, bulk actions, and row actions all work exactly as before.
CSS Classes
All elements use fi-responsive-table-* prefixed classes for targeted styling:
| Class | Element |
|---|---|
fi-responsive-table |
Root wrapper (carries data-breakpoint) |
fi-responsive-table-desktop |
Wraps Filament's native table |
fi-responsive-table-cards |
Wraps the card stack |
fi-responsive-table-cards-list |
Inner flex container for cards |
fi-responsive-table-card |
Individual card |
fi-responsive-table-card-header |
Card title + optional checkbox bar |
fi-responsive-table-card-title |
Title text |
fi-responsive-table-card-checkbox |
Bulk-selection checkbox |
fi-responsive-table-card-body |
<dl> grid of label/value pairs |
fi-responsive-table-card-field |
One label/value pair (uses display: contents) |
fi-responsive-table-card-field-label |
Column label (<dt>) |
fi-responsive-table-card-field-value |
Rendered column value (<dd>) |
fi-responsive-table-card-footer |
Record-actions footer |
Override any of these in your theme CSS to customize the card appearance.
License
MIT