steelants/datatable
A Laravel package for building dynamic data tables with server-side processing, column configuration, and search support.
The package is small enough to read end-to-end before you write a line of code, and you should — with 2 stars and no dependents there is no blog-post ecosystem to fall back on, so src/ and resources/views/ are the documentation.
composer require steelants/datatablephp artisan vendor:publish --provider="Steelants\...\DataTableServiceProvider" (run php artisan vendor:publish with no args and look for the package's tag — it will list the available tags even if you don't publish).DataTable class or trait in src/ — this defines every method you'll call,composer.json — check the laravel/framework and livewire/livewire constraints, because a Livewire 2/3 mismatch will be your first real error.First use case: a paginated, sortable, searchable index of one Eloquent model, wired into an existing page. Create a dedicated Livewire component (don't try to bolt the datatable onto an existing form component), point it at the model's query, declare columns, and render the table in its view. That single vertical slice will expose whether the package's search/sort/pagination assumptions match your app's conventions.
One Livewire component per table. Treat the datatable component as a thin shell: mount() builds the query, the component's public properties hold search/sort/page state, and the Blade view renders the package's table markup. If you find yourself sharing a component across two resources, you almost certainly want two components instead — the query construction is the only thing that differs, and over-generalizing here is where these packages get painful.
Column definitions live in PHP, rendering lives in Blade. Define the column list (key, label, sortable flag) in the component, then use the package's cell rendering hook for anything non-trivial — badges, formatted dates, links, relation names. Keep raw Eloquent attribute output as the default path and only reach for a custom renderer when you need it.
Eager-load aggressively in mount(). Any column that touches a relation ($row->customer->name) must be backed by ->with('customer') on the base query. This is the single biggest performance lever and the package will not warn you about it.
Sorting and searching map to the query builder. The typical shape is a base Builder stored on the component, then the package applies orderBy/where ... like/paginate based on public properties. Validate the sort column against your whitelist before it hits orderBy — see Gotchas.
Actions column as the last column. Row-level edit/delete buttons belong in a dedicated non-sortable column, using wire:click with the record ID and wire:key on the row. Keep the confirm-and-delete logic in the component, not in the view.
Integration with existing pages: mount the component from a Blade page via <livewire:admin.users-table /> rather than trying to embed it in a controller-rendered view. If you need the table state in the URL, push search/sort/page onto the query string via Livewire's #[Url] (v3) or $queryString (v2) on the component's own properties.
Bulk actions and export: if the package supports row selection, wire the selected IDs into a component property and do the work in a component method with a $this->dispatch('...') refresh afterwards. If it doesn't, add selection yourself — it's a checkbox column plus an array property, and it's usually less work than fighting the package.
orderBy, an attacker controls an identifier position. Always interpose a whitelist: abort_unless(in_array($sort, $allowedColumns), 422). Same for any filter/search parameter that touches column names.$emit vs dispatch), query-string attributes, and pagination view names all differ. Pin the package version in composer.json and read the constraint before upgrading Livewire.$this->resetPage() in v3 / resetPage() for the WithPagination trait). Otherwise a user on page 7 searching for something with two results sees an empty table and files a bug.wire:key on rows. Without a stable key on each <tr>, Livewire reuses DOM nodes on delete/sort and you get stale checkboxes and mismatched action buttons.DB::listen or Laravel Debugbar once. If the query count scales with page size, you're missing with() — the datatable makes this easy to miss because the page still works, just slowly.paginate() on a table with LIKE '%term%' search will table-scan. If rows exceed a few hundred thousand, switch the search to a whereFullText or swap the base query for a Scout-backed source rather than tuning the package.dump($this->all()) in the render method tells you what the table thinks the search/sort/page state is, which resolves most "the table won't update" reports faster than reading package source.DataTable class is small enough to fork into app/Support/ and maintain yourself; treat the package as a starting point rather than a dependency of record.How can I help you explore Laravel packages today?