wezlo/filament-responsive-table

Render a Filament list table as stacked cards below a configurable Tailwind breakpoint.

Maintainers

Package info

github.com/mustafakhaleddev/filament-responsive-table

pkg:composer/wezlo/filament-responsive-table

Statistics

Installs: 25

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.0 2026-04-22 01:11 UTC

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, 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 filteringonly() / 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 $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.

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:

  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 fileconfig/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 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 <div> carries data-breakpoint="<bp>". 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 <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