ramonrietdijk/livewire-tables
A Laravel Livewire package for building dynamic, responsive data tables with sorting, searching, filtering, pagination, and customizable columns and views. Drop it into any Livewire component to render powerful, interactive tables with minimal code.
composer require ramonrietdijk/livewire-tables
php artisan vendor:publish --tag="livewire-tables-config"
# only if you intend to restyle the markup:
php artisan vendor:publish --tag="livewire-tables-views"
The package assumes Laravel 10/11, PHP 8.1+, Livewire 3 and Tailwind (the default views are Tailwind classes, so a Bootstrap app means republishing and rewriting the view).
A table is a Livewire component that extends the package base class and names a model:
namespace App\Livewire;
use App\Models\User;
use RamonRietdijk\LivewireTables\Livewire\LivewireTable;
use RamonRietdijk\LivewireTables\Columns\Column;
class UserTable extends LivewireTable
{
protected string $model = User::class;
public function columns(): array
{
return [
Column::make('id', 'ID'),
Column::make('name', 'Name'),
Column::make('email', 'Email'),
];
}
}
<livewire:user-table />
That alone gives you pagination, global search, sorting and per-page selection — which is exactly the boilerplate it exists to remove.
Where to look first: vendor/ramonrietdijk/livewire-tables/src/Livewire/LivewireTable.php. That class is the contract — every public/protected method is an extension point. Then read src/Columns/Column.php for the fluent column API, and skim src/Filters and src/Actions so you know what ships before writing your own.
First use case: an admin index page you'd otherwise hand-roll. Replace the controller + Blade loop with one table class, keep your existing show/edit routes as column links.
Columns cover the three cases you actually hit. Database columns by name, relations by dot notation, and non-SQL values as computed:
public function columns(): array
{
return [
Column::make('company.name', 'Company'), // eager-loads via dot notation
Column::make('created_at', 'Created')->format(fn ($value) => $value->format('d-m-Y')),
Column::make('is_admin', 'Admin')->asBoolean(),
Column::make('full_name', 'Name')
->computed(fn (User $user) => "{$user->first_name} {$user->last_name}"),
];
}
Toggle behaviour per column rather than globally (->searchable(), ->sortable(), and their false variants). Leaving search enabled on a JSON or accessor column is the fastest way to get a confusing SQL error.
Scope the query, don't filter the results. Multi-tenancy, soft-deleted handling and "only published" belong in the query hook, so counts, pagination and bulk actions all agree:
protected function query(Builder $query): Builder
{
return $query->where('team_id', auth()->user()->team_id);
}
Filters are cheap wins for the UI. Select/date/search filters keep state in the component and compose with the query, so you get "filter + search + sort + paginate" without writing a single if ($request->has(...)).
Actions replace ad-hoc buttons. Bulk delete, export, "mark as reviewed" — define them once with a callback receiving the selected models. Always re-authorize inside the callback: the UI flag hides the button, it does not protect the endpoint.
Wire it into your page three ways: as the whole page (Route::get('/users', UserTable::class)), nested in a Blade view for dashboard layouts, or embedded alongside other widgets. For the latter, give each table a distinct wire:key and its own wrapper element.
Testing. These are ordinary Livewire components, so Livewire::test(UserTable::class)->assertSee('...')->call('...') works. Assert on the query behaviour (scoping, filters applied) rather than on markup — the Blade is the part most likely to be republished and changed.
with/query rather than relying on lazy loading.computed() for display only.resources/, add the package's view path to content in tailwind.config.js — otherwise the table renders unstyled in production only.can-style flags are presentation. Any route or action the table exposes still needs a policy check server-side.rappasoft/laravel-livewire-tables. Both can coexist but the Column/Action/Filter imports become ambiguous fast. Pick one table package per project and import with aliases if you must.query() (->dd()), inspect getQueryString() for state bugs, and use Livewire's devtools for request payloads. Most "it doesn't filter" reports are state not reaching the query, not the query being wrong.vendor/), and re-read the CHANGELOG before upgrading — release activity as of the 2026-08-20 tag suggests it's alive, not abandoned.Column/Filter/Action → override methods on your table class → publish and edit views → patch the package. Stay in the first two and upgrades stay boring.How can I help you explore Laravel packages today?