https://github.com/mustafakhaleddev/filament-responsive-table
Filament Responsive Table
https://github.com/mustafakhaleddev/filament-responsive-table
Last synced: 8 days ago
JSON representation
Filament Responsive Table
- Host: GitHub
- URL: https://github.com/mustafakhaleddev/filament-responsive-table
- Owner: mustafakhaleddev
- Created: 2026-04-22T01:10:37.000Z (2 months ago)
- Default Branch: master
- Last Pushed: 2026-04-22T01:11:41.000Z (2 months ago)
- Last Synced: 2026-06-08T00:06:54.086Z (22 days ago)
- Language: PHP
- Size: 9.77 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Filament Responsive Table
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`, or `2xl`; 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
```bash
composer require wezlo/filament-responsive-table
```
Optionally register the plugin in your Panel Provider for global defaults:
```php
use Wezlo\FilamentResponsiveTable\FilamentResponsiveTablePlugin;
->plugins([
FilamentResponsiveTablePlugin::make()
->defaultBreakpoint('md'),
])
```
Optionally publish the config:
```bash
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`):
```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:
```bash
php artisan make:filament-theme
```
Then rebuild assets:
```bash
npm run build
```
## Quick Start
Add the `HasResponsiveTable` trait to your resource's `ListRecords` page and declare a `$responsiveBreakpoint` property to pick the breakpoint:
```php
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 `$responsiveBreakpoint` itself (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-nullable `string` with 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.
```php
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` | Keep only these column names in cards. Desktop table is untouched. |
| `except(array)` | `array` | 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.
```php
$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).
```php
$config->cardTitle(fn ($record) => "#{$record->invoice_number}");
$config->cardTitle(fn ($record) => new HtmlString("{$record->name}"));
```
### Custom Card View
Point to your own Blade view if the default layout doesn't fit:
```php
$config->cardView('users.mobile-card');
```
The view receives three variables:
| Variable | Type | Description |
|---|---|---|
| `$record` | `Model` | The Eloquent record for this card |
| `$columns` | `array` | 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:
```php
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:
1. **`responsiveTable()` method** on the `ListRecords` page (highest priority)
2. **`$responsiveBreakpoint` property** on the `ListRecords` page (breakpoint only)
3. **Plugin defaults** on `FilamentResponsiveTablePlugin` in the Panel Provider
4. **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
```php
// config/filament-responsive-table.php
return [
'breakpoint' => 'md',
'show_record_actions' => true,
'show_bulk_selection' => false,
];
```
## Full Example
```php
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 `HasResponsiveTable` trait overrides `content()` on the `ListRecords` page 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 `
` carries `data-breakpoint=""`. A small shipped stylesheet has static `@media` rules — 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 applies `only`/`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 by `isHidden()`, 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` | `
` 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 (`- `) |
| `fi-responsive-table-card-field-value` | Rendered column value (`
- `) |
| `fi-responsive-table-card-footer` | Record-actions footer |
Override any of these in your theme CSS to customize the card appearance.
## License
MIT