hydrat/filament-table-layout-toggle
Adds a table/grid layout toggle to Filament resource tables, letting users switch between standard table rows and card-style grid views while keeping Filament's familiar filtering, sorting, and search features.
Install the plugin:
composer require hydrat/filament-table-layout-toggle
Register the plugin on your panel (usually app/Providers/Filament/AdminPanelProvider.php):
use Hydrat\TableLayoutToggle\TableLayoutTogglePlugin;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(TableLayoutTogglePlugin::make());
}
Publish the config if you need to tweak defaults (persistence, default layout, switcher style):
php artisan vendor:publish --tag="table-layout-toggle-config"
Add the trait to a List page. Open the resource's ListRecords page and use HasToggleableTable:
namespace App\Filament\Resources\UserResource\Pages;
use Filament\Resources\Pages\ListRecords;
use Hydrat\TableLayoutToggle\Concerns\HasToggleableTable;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ImageColumn;
class ListUsers extends ListRecords
{
use HasToggleableTable;
protected function getGridColumns(): array
{
return [
ImageColumn::make('avatar')->circular(),
TextColumn::make('name')->weight('bold'),
TextColumn::make('email')->color('gray'),
];
}
}
First use case: a directory/contacts screen where the table is best for bulk actions and sorting, but the grid is better for browsing by avatar. Same Livewire query, same filters — only the render mode changes. Open the resource, click the toggle in the table header, and the page re-renders between table and card layout without a full reload.
Where to look first: the config file (config/table-layout-toggle.php) and the trait source (vendor/hydrat/filament-table-layout-toggle/src/Concerns/HasToggleableTable.php) — that's where the layout state, view, and defaults live.
Layout state lives on the Livewire page. The trait exposes a $layout (or equivalent) property that's toggled by a table header action. Because it's page state, filters, search, and pagination survive the switch — you get the same result set rendered differently.
Separate grid columns from table columns. You'll usually want different columns in each mode. Keep table(Table $table) returning sortable, action-heavy columns (checkbox, bulk actions, row actions), and return lean "card-shaped" columns from getGridColumns() — image, title, subtitle, badges. Trying to reuse one set of columns for both modes is the #1 source of ugly grids.
Persistence. By default the plugin can remember the user's choice. Configure in config/table-layout-toggle.php:
persist_layout_in_session (or the storage driver config) — set to true so users don't flip back to table on every page load.default_layout — 'table' for CRUD-heavy resources (invoices, logs), 'grid' for media-heavy resources (products, contacts, files).Switcher style. The config typically lets you choose between a toggle (single icon button) and a select (dropdown). Use toggle for two-state layouts, select if you later add a third mode (e.g. list, grid, table). Match what's already in your header — don't mix a plain toggle with the rest of your table-header actions styled as dropdowns.
Per-resource, not global. Apply the trait only on pages where a grid actually makes sense. There's no value in enabling it on an audit-log resource. The trait is a concern — cheap to add per-page, and you keep the default table behavior everywhere else.
Combine with filters for power-user flows. Grid mode reads from the same Table query, so ->filters([...]) still work. A common pattern: filters on status/category + grid layout so a merchandiser can scan products visually, then hit row actions (edit/delete) directly on the card.
Card composition via columns. Prefer ImageColumn + TextColumn + IconColumn/BadgeColumn over a custom Blade view for the first pass. It keeps everything fluent and theme-consistent. Reach for a Blade view only when you need genuinely custom card markup (multi-column stats, nested tags).
Integration with other table plugins. If you're combining with e.g. column toggles, filters panels, or a row-reordering plugin, register this trait after any trait that also touches table()/getTableColumns(). Order matters because traits resolve method conflicts at class-definition time.
The grid doesn't automatically know your table columns. If getGridColumns() is empty or missing, expect either a fallback render (all table columns stacked, looking messy) or an empty grid. Always define it explicitly for anything user-facing.
Bulk actions and checkboxes are table-only concepts. Don't try to preserve ->bulkActions() selection state across the toggle — the UI differs and users will get confused about what's selected. If bulk editing matters, either disable the toggle on that resource or accept the context switch.
Persistence is per-session/user, not per-resource out of the box. Check the config; if you need per-resource memory (e.g. "this user prefers grid on Products but table on Invoices"), verify how the state key is namespaced. If it's shared, you may need to override the key in the trait method or keep it global-and-consistent as a UX decision.
Livewire re-render quirks. Toggling triggers a Livewire round-trip. If you've got computed properties or expensive queries inside getGridColumns(), they'll fire on every toggle. Cache or memoize anything heavy.
Mobile is your blind spot. Grid card widths configured as "3 columns" assume desktop viewport. Test at sm/md — cards often need grid-cols-1 on mobile. If the plugin's default grid view doesn't expose Tailwind breakpoints, publish the view and adjust.
Empty states. In grid mode an empty table can look like a broken page because there's less chrome around it. Verify ->emptyStateHeading() / ->emptyStateDescription() still show up (they should, but confirm on a fresh local DB).
Row actions in grid mode. Whether row actions (edit/delete icons) render on cards depends on the plugin version. If they don't appear and you need them, put them in the card via a custom Blade grid view or add a LinkAction column. Verify before shipping.
Debouncing / search. Search input still works in both modes because it's table-level state, but the UX of a search box above cards can feel odd — consider whether the grid view needs a heading like "N results" for context.
Extension point: publish the grid blade view. When fluent columns aren't enough, publish views (php artisan vendor:publish --tag="table-layout-toggle-views") and edit the card partial. Keep your edits in a namespaced override rather than editing vendor/ — otherwise composer update wipes them.
Trait conflicts. If multiple plugins add a header action on the table, they all pile up. If the toggle button is missing, first check php artisan filament:optimize-clear and confirm the plugin is registered — a missing ->plugin(...) call is the most common "it's not showing up" cause.
How can I help you explore Laravel packages today?