A Blade-centric, reactive data table component for Laravel + Livewire. Define your tables using familiar Blade component syntax and get sorting, searching, filtering, and pagination out of the box — all powered by Livewire under the hood.
<x-data-table> and its sub-componentsvanilla, bootstrap4v1, and tailwind3v1 presets; fully customizable via configmax and all special values@rowClass, @rowStyle, and @rowAttributes directives<x-data-table.footer>| Dependency | Version |
|---|---|
| PHP | ^8.3 |
| Laravel | ^12.60 or ^13.10 |
| Livewire | ^3.6 or ^4.0 |
| erickcomp/laravel-raw-blade-components | 2.0 (installed automatically) |
| phpgt/dom | ^4.1 (installed automatically) |
| Package | Purpose |
|---|---|
| laravie/serialize-queries | Required when using Eloquent/Query builder instances as data sources |
composer require erickcomp/livewire-data-table
The package uses Laravel's auto-discovery, so the service provider is registered automatically.
Publish the configuration file:
php artisan vendor:publish --tag=erickcomp-livewire-data-table-config
Publish the translation files:
php artisan vendor:publish --tag=erickcomp-livewire-data-table-lang
Note: This package stores compiled component definitions in
storage/framework/views/, alongside Laravel's compiled Blade templates. They are automatically cleared byphp artisan view:clear, which is typically part of deployment scripts.
The configuration file (config/erickcomp-livewire-data-table.php) contains:
columns-search-debounce-ms, query string parameter namesempty, vanilla, bootstrap4v1, and tailwind3v1Each preset defines CSS classes, icons, transitions, loader overlays, pagination views, and asset styles for every table element. Presets support inheritance via the extends key.
You can also define custom presets in your application config under the app.lw_dt_presets key:
// config/app.php (or a dedicated config file)
'lw_dt_presets' => [
'my-theme' => [
'extends' => 'vanilla',
'table' => [
'class' => ['my-custom-table-class'],
],
// ... override any preset keys
],
],
<x-data-table :data-src="App\Models\User::class" preset="tailwind3v1">
<x-data-table.column title="ID" data-field="id" sortable />
<x-data-table.column title="Name" data-field="name" sortable searchable />
<x-data-table.column title="Email" data-field="email" sortable searchable />
<x-data-table.column title="Created At" data-field="created_at" sortable />
</x-data-table>
<x-data-table :data-src="$users" preset="bootstrap4v1" per-page="10,25,50">
<x-data-table.column title="Name" data-field="name" sortable />
<x-data-table.column title="Role" data-field="role" />
</x-data-table>
Requires the
laravie/serialize-queriespackage.
<x-data-table :data-src="App\Models\User::where('active', true)" preset="vanilla">
<x-data-table.column title="Name" data-field="name" sortable searchable />
<x-data-table.column title="Email" data-field="email" />
</x-data-table>
<x-data-table data-src="App\Services\UserService@getUsers" preset="tailwind3v1">
<x-data-table.column title="Name" data-field="name" sortable />
</x-data-table>
The callable receives an LwDataRetrievalParams instance with all search, filter, sort, and pagination parameters:
namespace App\Services;
use ErickComp\LivewireDataTable\Livewire\LwDataRetrievalParams;
use App\Models\User;
class UserService
{
public function getUsers(LwDataRetrievalParams $params)
{
$query = User::query();
// Apply search, filters, sorting and paginate in one call
return $params->applyAndPaginate($query);
}
}
<x-data-table :data-src="App\Models\User::class" preset="tailwind3v1">
<x-data-table.search data-fields="name,email" />
<x-data-table.column title="Name" data-field="name" sortable />
<x-data-table.column title="Email" data-field="email" sortable />
</x-data-table>
The data-fields attribute accepts a comma-separated list of fields to search across. You can also specify search modes per field:
<x-data-table.search data-fields="name:contains,email:starts_with,bio:fulltext" />
Available search modes: contains (default), starts_with, ends_with, exact, fulltext.
Setting data-fields to true (or omitting it) searches all non-hidden model columns when using an Eloquent data source. When using a collection-based data source, data-fields must be explicitly provided.
Add searchable to column definitions to enable per-column search inputs:
<x-data-table.column title="Name" data-field="name" searchable />
<x-data-table.column title="Email" data-field="email" searchable="starts_with" />
The searchable attribute accepts true (defaults to contains mode) or a specific mode string.
<x-data-table :data-src="App\Models\Order::class" preset="tailwind3v1">
<x-data-table.filters collapsible="true">
<x-data-table.filter
data-field="status"
label="Status"
input-type="select"
/>
<x-data-table.filter
data-field="created_at"
label="Date Range"
input-type="date"
mode="range"
/>
<x-data-table.filter
data-field="total"
label="Amount"
input-type="number"
mode="range"
/>
<x-data-table.filter
data-field="customer_name"
label="Customer"
input-type="text"
mode="contains"
/>
</x-data-table.filters>
<x-data-table.column title="Order #" data-field="id" sortable />
<x-data-table.column title="Customer" data-field="customer_name" sortable />
<x-data-table.column title="Total" data-field="total" sortable />
<x-data-table.column title="Status" data-field="status" sortable />
<x-data-table.column title="Date" data-field="created_at" sortable />
</x-data-table>
| Type | Description |
|---|---|
text |
Text input (default) |
number |
Number input |
date |
Date text input |
date-picker |
Native date picker |
datetime |
Datetime text input |
datetime-picker |
Native datetime-local picker |
select |
Dropdown select |
select-multiple |
Multiple-selection dropdown |
| Mode | SQL Equivalent |
|---|---|
exact |
= value |
contains |
LIKE %value% |
starts_with |
LIKE value% |
ends_with |
LIKE %value |
fulltext |
MATCH ... AGAINST |
range |
>= from AND <= to |
IN |
IN (values) |
You can also specify the mode inline in the data-field attribute:
<x-data-table.filter data-field="name:starts_with" label="Name" />
Wrap content inside <x-data-table.filter> to provide a custom template:
<x-data-table.filter data-field="status" label="Status">
<select>
<option value="">All</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</x-data-table.filter>
The package automatically injects x-model and name attributes into <input> and <select> elements within custom filter templates.
Wrap content inside <x-data-table.column> to define custom cell rendering:
<x-data-table.column title="Actions" data-field="id">
<a href="/users/{{ $__row->id }}/edit">Edit</a>
</x-data-table.column>
Use the @rowClass, @rowStyle, and @rowAttributes directives on <x-data-table> to dynamically style rows based on row data. The variable $__row represents the current row and $loop is the standard loop variable:
<x-data-table
:data-src="App\Models\User::class"
preset="tailwind3v1"
@rowClass(['bg-red-100' => $__row->is_blocked])
@rowStyle(['opacity: 0.5' => $__row->is_inactive])
@rowAttributes(['data-user-id' => $__row->id])
>
<x-data-table.column title="Name" data-field="name" />
</x-data-table>
<x-data-table :data-src="$data" preset="vanilla">
<x-data-table.column title="Product" data-field="product" />
<x-data-table.column title="Amount" data-field="amount" />
<x-data-table.footer>
<tr>
<td><strong>Total</strong></td>
<td>{{ $data->sum('amount') }}</td>
</tr>
</x-data-table.footer>
</x-data-table>
Inject custom CSS or JavaScript alongside the table:
<x-data-table :data-src="$data" preset="vanilla">
<x-data-table.assets>
<style>
.my-custom-style { color: red; }
</style>
</x-data-table.assets>
<x-data-table.column title="Name" data-field="name" />
</x-data-table>
Custom column renderers, custom filter renderers, footer templates, and custom search templates run inside isolated Blade::render() scopes — they don't inherit variables from the parent view. To pass data into these renderers, use dynamic view data and/or static view data.
dynamic-view-data)Data that varies per user or per request — permissions, session state, locale, user preferences.
How it works: Serialized and encrypted into a Livewire property on mount. Travels over the wire on every Livewire update (sort, filter, paginate). Decrypted and unserialized on the server for each render cycle.
When to use: The data is different for each user viewing the same page. Storing it in the compiled cache would create a separate cache file per user — wasteful. Instead, it rides along with the Livewire snapshot.
Keep it small. Every byte in dynamic view data is sent on every interaction.
<x-data-table
:data-src="App\Models\Order::class"
preset="tailwind3v1"
:dynamic-view-data="[
'permissions' => $userPermissions,
'locale' => app()->getLocale(),
]"
>
<x-data-table.column title="Actions" data-field="id">
@if($permissions->canEdit)
<a href="/orders/{{ $__row->id }}/edit">Edit</a>
@endif
</x-data-table.column>
</x-data-table>
static-view-data)Data that is the same for all users — ViewModels, formatters, configuration objects, app settings.
How it works: Serialized with the compiled DataTable into storage/framework/views/. Loaded from disk on every Livewire hydration. Never sent over the wire — zero network overhead.
When to use: The data doesn't change between users or requests. A ViewModel with convenience methods, a currency formatter, app config values — these are identical for every visitor. Compiling them into the cache file means they're loaded from disk (fast) instead of transmitted over the network (slow for large objects).
Trade-off: Different static view data values produce different cache files. This is fine for shared objects (one file serves all users), but would cause cache proliferation if the values varied per user.
@php
$vm = new App\ViewModels\OrderTableViewModel();
@endphp
<x-data-table
:data-src="App\Models\Order::class"
preset="tailwind3v1"
:static-view-data="[
'vm' => $vm,
'currency' => $currency,
'appName' => config('app.name'),
]"
>
<x-data-table.column title="Status" data-field="status">
<span class="{{ $vm->statusClass($__row->status) }}">
{{ $vm->statusLabel($__row->status) }}
</span>
</x-data-table.column>
<x-data-table.column title="Total" data-field="total">
{{ $currency->format($__row->total) }}
</x-data-table.column>
<x-data-table.footer>
<tr>
<td colspan="2">{{ $appName }} — all values in {{ $currency->code }}</td>
</tr>
</x-data-table.footer>
</x-data-table>
Use both attributes together when your table needs shared configuration and per-user context. When keys overlap, dynamic view data takes precedence over static:
<x-data-table
:data-src="App\Models\Order::class"
:static-view-data="['vm' => $vm, 'currency' => $currency]"
:dynamic-view-data="['permissions' => $userPermissions]"
>
<x-data-table.column title="Total" data-field="total">
{{ $currency->format($__row->total) }}
</x-data-table.column>
<x-data-table.column title="Actions" data-field="id">
@if($permissions->canEdit)
<a href="{{ $vm->editUrl($__row->id) }}">Edit</a>
@endif
</x-data-table.column>
</x-data-table>
| Data type | Use | Why |
|---|---|---|
| ViewModels with helper methods | static-view-data |
Same for all users, avoids wire overhead |
| Currency / number formatters | static-view-data |
Shared configuration, loaded once from disk |
App config (config('app.name')) |
static-view-data |
Never changes between requests |
| User permissions | dynamic-view-data |
Different per user, avoids per-user cache files |
| Session or auth state | dynamic-view-data |
Changes per user |
| Small strings or booleans | Either | Negligible cost both ways |
| Large collections or DTOs | static-view-data |
Avoids bloating every Livewire round-trip |
Both types are merged and available in all Blade::render() contexts:
<x-data-table.column> tags)<x-data-table.filter> tags)<x-data-table.search> tags)<x-data-table.footer> tags)Serializable or __serialize()/__unserialize(). Closures and anonymous classes are not supported.{{ app(MyService::class)->method() }}.$__row, $loop, $attributes, $___lwDataTable). Internal variables always take precedence.The component supports multiple pagination types via the data-src-pagination attribute:
| Value | Type |
|---|---|
length_aware |
Full pagination with page numbers (default) |
simple |
Previous/Next only |
cursor |
Cursor-based pagination |
none |
No pagination |
<x-data-table
:data-src="App\Models\User::class"
data-src-pagination="simple"
per-page="10,25,50,all"
>
...
</x-data-table>
Special per-page values:
max — resolves to the max-per-page value (default: 1000)all — shows all records (translatable label)Control when the loading overlay appears using Livewire delay modifiers:
<x-data-table :data-src="App\Models\User::class" loading-delay-modifier="short">
...
</x-data-table>
Valid values: shortest, shorter, short, long, longer, longest.
For large datasets, you can increase the memory limit per table:
<x-data-table :data-src="App\Models\LargeTable::class" php-max-memory="512M">
...
</x-data-table>
<x-data-table>| Attribute | Type | Default | Description |
|---|---|---|---|
data-src |
string|iterable|Builder|callable |
null |
Data source for the table |
preset |
string |
'empty' |
UI preset name |
per-page |
string|array |
Model default or [15] |
Comma-separated per-page options |
max-per-page |
int |
1000 |
Maximum allowed per-page value |
data-src-pagination |
string |
'length_aware' |
Pagination type |
page-name |
string |
'page' |
Query string parameter for page |
search-name |
string |
'search' |
Query string parameter for search |
filters-name |
string |
'filters' |
Query string parameter for filters |
columns-search-name |
string |
'cols-search' |
Query string parameter for column search |
columns-search-debounce |
int |
250 |
Debounce in ms for column search |
pagination-view |
string |
Preset default | Custom pagination view |
loading-delay-modifier |
string |
null |
Livewire loading delay modifier |
data-identity-column |
string |
'id' |
Row identity column |
php-max-memory |
string |
null |
PHP memory_limit override |
dynamic-view-data |
array |
[] |
Per-user/per-request variables passed to Blade renderers (encrypted, travels over the wire) |
static-view-data |
array |
[] |
Shared variables passed to Blade renderers (compiled into cache file, zero wire overhead) |
collection-sorting-flags |
int|string |
SORT_NATURAL | SORT_FLAG_CASE |
Sorting flags for collection data sources |
HTML attributes prefixed with specific namespaces are forwarded to internal elements:
| Prefix | Target Element |
|---|---|
container- |
Main wrapper <div> |
table-wrapper- |
Table wrapper <div> |
thead- |
<thead> |
thead-tr- |
Header <tr> |
thead-search-tr- |
Column search <tr> |
thead-search-th- |
Column search <th> |
th- |
Header <th> |
tbody- |
<tbody> |
tbody-tr- |
Body <tr> |
| (no prefix) | <table> |
<x-data-table.column>| Attribute | Type | Default | Description |
|---|---|---|---|
title |
string |
required | Column header text |
data-field |
string |
null |
Data key to display |
sortable |
bool |
false |
Enable sorting |
searchable |
bool|string |
false |
Enable per-column search (true or mode) |
Supports th-*, td-*, and th-search-input-* prefixes for forwarding attributes.
<x-data-table.search>| Attribute | Type | Default | Description |
|---|---|---|---|
data-fields |
string|array|true |
true |
Fields to search (comma-separated, or true for all) |
Supports input-* and button-* prefixes for forwarding attributes.
<x-data-table.filter>| Attribute | Type | Default | Description |
|---|---|---|---|
data-field |
string |
required | Column to filter on |
label |
string |
Auto-generated from data-field | Display label |
name |
string |
From data-field |
HTML input name |
input-type |
string |
'text' |
Input type (see table above) |
mode |
string |
Auto from input-type | Filter mode (see table above) |
Supports from-* and to-* prefixes for range filter input attributes.
<x-data-table.filters>| Attribute | Type | Default | Description |
|---|---|---|---|
collapsible |
bool |
Preset default | Whether the filter container is collapsible |
row-length |
int |
4 |
Number of filter items per row |
Supports button-toggle-*, button-apply-*, and filter-item-* prefixes for forwarding attributes.
Implement these interfaces on your Eloquent models to customize how the data table interacts with your data:
CustomizesDataTableQueryProvide a custom base query (e.g., eager-load relationships):
use ErickComp\LivewireDataTable\Data\Eloquent\CustomizesDataTableQuery;
class User extends Model implements CustomizesDataTableQuery
{
public function dataTableQuery(): EloquentBuilder
{
return $this->newQuery()->with(['roles', 'department']);
}
}
CustomizesDataTableResultsTake full control of data retrieval:
use ErickComp\LivewireDataTable\Data\Eloquent\CustomizesDataTableResults;
class User extends Model implements CustomizesDataTableResults
{
public function dataTableData(
EloquentBuilder $query,
LwDataRetrievalParams $params,
): LengthAwarePaginator {
return $query->paginate($params->perPage);
}
}
CustomizesDataTableSearchOverride global search behavior:
use ErickComp\LivewireDataTable\Data\Eloquent\CustomizesDataTableSearch;
class User extends Model implements CustomizesDataTableSearch
{
public function applyDataTableSearch(EloquentBuilder $query, LwDataRetrievalParams $params)
{
$query->where(function ($q) use ($params) {
$q->whereLike('first_name', "%{$params->search}%")
->orWhereLike('last_name', "%{$params->search}%");
});
}
}
CustomizesDataTableColumnsSearchOverride per-column search behavior:
use ErickComp\LivewireDataTable\Data\Eloquent\CustomizesDataTableColumnsSearch;
class User extends Model implements CustomizesDataTableColumnsSearch
{
public function applyDataTableColumnsSearch(EloquentBuilder $query, LwDataRetrievalParams $params)
{
foreach ($params->columnsSearch as $field => $value) {
$query->whereLike($field, "%{$value}%");
}
}
}
CustomizesDataTableSortingOverride sorting behavior:
use ErickComp\LivewireDataTable\Data\Eloquent\CustomizesDataTableSorting;
class User extends Model implements CustomizesDataTableSorting
{
public function applyDataTableSorting(EloquentBuilder $query, LwDataRetrievalParams $params)
{
if ($params->sortBy === 'full_name') {
$query->orderBy('first_name', $params->sortDir)
->orderBy('last_name', $params->sortDir);
return;
}
$query->orderBy($params->sortBy, $params->sortDir);
}
}
LwDataRetrievalParams APIWhen using a callable data source, the LwDataRetrievalParams object provides convenience methods:
| Method | Description |
|---|---|
apply($data) |
Apply search, filters, and sorting to a query or collection |
applyAndPaginate($data) |
Apply all params and return a LengthAwarePaginator |
applyAndSimplePaginate($data) |
Apply all params and return a Paginator |
applyAndCursorPaginate($data) |
Apply all params and return a CursorPaginator |
paginate($data) |
Paginate without applying search/sort/filter |
simplePaginate($data) |
Simple paginate without applying params |
cursorPaginate($data) |
Cursor paginate without applying params |
| Property | Type | Description |
|---|---|---|
$params->page |
?int |
Current page number |
$params->perPage |
?string |
Items per page |
$params->pageName |
string |
Query string parameter name for page |
$params->search |
?string |
Global search term |
$params->columnsSearch |
?array |
Per-column search values (['field' => 'value']) |
$params->filters |
?array |
Applied filters |
$params->sortBy |
?string |
Sort column |
$params->sortDir |
?string |
Sort direction (ASC or DESC) |
Presets define the complete visual appearance of the data table. The package ships with:
| Preset | Description |
|---|---|
empty |
Bare minimum — no styles, serves as a base for other presets |
vanilla |
Minimal CSS with lw-dt-* class names and built-in styles |
bootstrap4v1 |
Bootstrap 4 compatible classes and components |
tailwind3v1 |
Tailwind CSS 3 utility classes with dark mode support |
Presets support single inheritance via the extends key, allowing you to override only the parts you need.
Presets are resolved from two config locations (first match wins):
config('app.lw_dt_presets.<name>')config('erickcomp-livewire-data-table.presets.<name>')Each preset can configure:
main-container — wrapper element classesactions — search, filters, per-page, and bulk actions containersearch — search input and button styling (with icon configuration)filters — filter container, toggle button, apply button, individual filter items, and input styling per typeapplied-filters — active filter badges and removal buttonstable — wrapper, thead, tbody, tfoot, th sorting indicatorsloader-overlay — loading spinner template and CSSpagination — views, container classes, per-page defaultsreload-alert — behavior when the serialized component cache expiresassets — CSS <style> blocks injected with the tablescripts — additional JavaScriptThe package includes translation files for:
en)pt_BR)Translation keys cover all UI labels: search button, filter labels, pagination text ("Showing X to Y of Z results"), "No data found" messages, per-page labels, and more.
To add a new locale, publish the translations and create a file at lang/vendor/erickcomp_lw_data_table/{locale}/messages.php.
The package uses Pest with Orchestra Testbench:
composer test
Static analysis with PHPStan (level 5):
composer analyse
Contributions are welcome! Please:
git checkout -b feature/my-feature)./vendor/bin/pest)This package is open-sourced software licensed under the MIT License.
How can I help you explore Laravel packages today?