Table
Wire Table
Enterprise-grade Livewire table component for Laravel. Depends on wire-core and wire-forms.
Bulk-selected rows with the active selection toolbar.
Enterprise-grade Livewire table component for Laravel. Depends on wire-core and wire-forms.
Installation
composer require nyoncode/wire-table
Add to Tailwind content paths:
module.exports = { content: [ // ... './vendor/nyoncode/wire-core/resources/views/**/*.blade.php', './vendor/nyoncode/wire-forms/resources/views/**/*.blade.php', './vendor/nyoncode/wire-table/resources/views/**/*.blade.php', ],}
Publish config (optional):
php artisan vendor:publish --tag=wire-table-config
Quick Start
use Livewire\Component;use NyonCode\WireTable\Concerns\WithTable;use NyonCode\WireTable\Table;use NyonCode\WireTable\Columns\TextColumn;use NyonCode\WireTable\Columns\BadgeColumn;use NyonCode\WireTable\Filters\SelectFilter;use NyonCode\WireCore\Actions\Action;use NyonCode\WireCore\Actions\DeleteAction;use NyonCode\WireCore\Actions\DeleteBulkAction; class UserTable extends Component{ use WithTable; public function table(Table $table): Table { return $table ->model(User::class) ->columns([ TextColumn::make('name') ->sortable() ->searchable() ->weight('bold'), TextColumn::make('email') ->searchable() ->copyable() ->copyMessage('Email copied!'), BadgeColumn::make('role') ->colors([ 'primary' => 'admin', 'success' => 'editor', 'gray' => 'viewer', ]), TextColumn::make('created_at') ->dateTime('d.m.Y') ->sortable() ->size('sm') ->textColor('gray'), ]) ->filters([ SelectFilter::make('role') ->options([ 'admin' => 'Admin', 'editor' => 'Editor', 'viewer' => 'Viewer', ]), ]) ->actions([ Action::make('edit') ->icon('pencil') ->url(fn (User $r) => route('users.edit', $r)), DeleteAction::make(), ]) ->bulkActions([ DeleteBulkAction::make(), ]) ->defaultSort('name') ->searchable() ->paginated() ->striped() ->hoverable(); } public function render() { return view('livewire.user-table'); }}
{{-- resources/views/livewire/user-table.blade.php --}}<div> {{ $this->table }}</div>
That's it. The table handles search, sort, filter, pagination, actions, and inline editing — all with zero JavaScript configuration.
WithTable Trait
The WithTable trait is the Livewire integration layer. It provides:
- All Livewire-bound public properties (search, sort, filters, pagination, selection)
- Lifecycle hooks (
mountWithTable, property watchers) - Query building via
TableQueryService - Action execution pipeline
- Inline editing pipeline
- Modal management
- Row expansion (sub-rows)
- Column visibility toggling
- SQL/query debugging
Public Properties (Livewire State)
These are automatically synced with the browser via Livewire:
| Property | Type | Default | Description |
|---|---|---|---|
$tableSearch |
?string |
null |
Current search term |
$tableSortColumn |
string |
'' |
Current sort column name |
$tableSortDirection |
string |
'asc' |
'asc' or 'desc' |
$tablePerPage |
int |
10 |
Records per page |
$tableFilters |
array |
[] |
Active filter values: ['role' => 'admin', ...] |
$columnFilters |
array |
[] |
Column-level filter values |
$selectedRecords |
array |
[] |
Primary keys of selected records |
$hiddenColumns |
array |
[] |
Column names hidden by user |
$expandedRows |
array |
[] |
Primary keys of expanded rows (sub-rows) |
$flattenMode |
bool |
false |
Show all sub-rows inline |
Livewire Methods (wire: callable)
These are called from Alpine.js or Livewire directives in the Blade views:
| Method | Called When |
|---|---|
sortTable($column) |
User clicks column header |
resetSort() |
User resets sort |
updatedTableSearch($value) |
Search input changes |
updatedTableFilters() |
Filter value changes |
updatedColumnFilters() |
Column filter changes |
updatedTablePerPage() |
Per-page selector changes |
toggleColumnVisibility($name) |
User hides/shows column |
selectRecord($key) |
Checkbox toggled |
selectAll() |
"Select all" toggled |
deselectAll() |
"Deselect all" clicked |
expandRow($key) |
Row expand/collapse |
toggleFlattenMode() |
Flatten sub-rows toggle |
executeAction($name, $key) |
Action button clicked |
executeBulkAction($name) |
Bulk action clicked |
updateCell($column, $key, $value) |
Inline edit committed |
confirmActionExecution() |
Modal "confirm" clicked |
cancelAction() |
Modal "cancel" clicked |
submitActionForm() |
Action form submitted |
Table Configuration API
The Table class provides a comprehensive fluent API. Below is the complete reference.
Data Source
// From Eloquent model class (auto-creates query)->model(string $modelClass) // Custom base query (overrides model)->query(Builder $query) // Modify the auto-generated query->modifyQueryUsing(Closure $fn) // Primary key column (default: 'id')->primaryKey(string $column)
Examples:
// Simple model$table->model(User::class); // Custom query with eager loads and scopes$table->query( User::query() ->where('tenant_id', auth()->user()->tenant_id) ->withCount(['posts', 'comments']) ->with(['department', 'team'])); // Modify auto-query$table->model(User::class) ->modifyQueryUsing(fn (Builder $q) => $q->where('active', true)); // UUID primary key$table->model(Order::class)->primaryKey('uuid');
Columns
->columns(array $columns)
See Columns Reference for all 13 column types.
Filters
->filters(array $filters)
See Filters Reference for all filter types.
Actions
// Row actions (per-record)->actions(array $actions) // Bulk actions (for selected records)->bulkActions(array $actions) // Header actions (table-level, no record context)->headerActions(array $actions) // Actions column position->actionsPosition(string 'start'|'end') // default: 'end' // Actions column alignment->actionsAlignment(string 'left'|'center'|'right') // Actions column header label->actionsColumnLabel(string $label) // Actions column fixed width->actionsColumnWidth(string $width) // e.g., '120px'
See Actions for the full Actions API.
Search
// Enable global search across all searchable columns->searchable(bool $searchable = true)
Search uses a database-aware strategy:
- MySQL:
MATCH ... AGAINSTfulltext (if index exists) orLIKE - PostgreSQL:
to_tsvector / ts_query - SQLite:
LIKE '%term%'fallback
Sorting
// Enable column header sorting->sortable(bool $sortable = true) // Default sort on initial load->defaultSort(string $column, string $direction = 'asc')
Pagination
// Enable pagination->paginated(bool $paginated = true) // Default per-page count->perPage(int $perPage = 10) // Per-page dropdown options->perPageOptions(array $options = [10, 25, 50, 100]) // Simple pagination — no COUNT(*) query, just Previous/Next->simplePagination() // Cursor pagination — offset-free, constant-time->cursorPagination() // Standard pagination (default) — full page numbers->standardPagination()
When to use which:
| Mode | Best For | Trade-offs |
|---|---|---|
| Standard | < 100k records, users need page numbers | COUNT(*) on every page load |
| Simple | 100k-1M records, sequential browsing | No total count, no page numbers |
| Cursor | > 1M records, real-time data | No random page access, opaque cursors |
Selection (Bulk Actions)
// Enable checkbox selection column->selectable(bool $selectable = true)
When enabled, checkboxes appear. Selected record keys are stored in $selectedRecords. Bulk actions operate on the selection.
Appearance
// Alternating row colors->striped(bool $striped = true) // Row hover highlight (default: true)->hoverable(bool $hoverable = true) // Reduced cell padding->compact(bool $compact = true) // Table/cell borders->bordered(bool $bordered = true) // Custom CSS class on <table> element->tableClass(string $class) // Custom CSS class on <thead>->headerClass(string $class) // Custom CSS class on <tr>->rowClass(string|Closure $class)
Dynamic row classes:
->rowClass(fn (User $record) => match(true) { $record->is_banned => 'bg-red-50 dark:bg-red-900/10', $record->is_admin => 'bg-blue-50 dark:bg-blue-900/10', default => '',})
Record URL (Clickable Rows)
// Make entire row clickable->recordUrl(string|Closure $url)
// With Closure->recordUrl(fn (User $record) => route('users.show', $record))
Responsive Layout
// Stack columns vertically on mobile->stackedOnMobile(bool $stacked = true) // Breakpoint below which to stack (default: 'md')->stackedBreakpoint(string $breakpoint) // 'sm', 'md', 'lg', 'xl'
Empty State
->emptyState(?string $heading = null, ?string $description = null, ?string $icon = null)
$table->emptyState( heading: 'No users found', description: 'Try adjusting your filters or search term.', icon: 'users',)
Polling (Auto-Refresh)
// Enable polling at interval->poll(string $interval = '5s') // Continue polling when browser tab is hidden->pollKeepAlive(bool $keepAlive = true) // Only poll when element is visible in viewport->pollOnlyVisible(bool $onlyVisible = true) // Conditional polling->pollWhen(Closure $condition) // Livewire method to call on poll (default: re-render)->pollMethod(string $method)
// Poll every 5s while there are pending jobs$table->poll('5s') ->pollWhen(fn () => Job::where('status', 'pending')->exists());
Lazy Loading
// Defer initial table render->lazy(bool $lazy = true) // Placeholder HTML during loading->lazyPlaceholder(string $html)
$table->lazy() ->lazyPlaceholder( '<div class="flex items-center justify-center p-12"> <x-wire::icon name="refresh" class="w-8 h-8 animate-spin text-gray-400" /> </div>' );
Performance
// Cache query results->cacheQuery(int $ttl, ?string $key = null) // Process records in chunks (for bulk operations)->chunk(int $size, Closure $callback)
// Cache for 60 seconds — key auto-generated from state hash$table->cacheQuery(60); // Custom cache key$table->cacheQuery(300, 'users-table');
Notifications
// Override notification driver for this table->notificationDriver(string $driver)
Debugging
// Get the QueryPlan object for inspection->debugQueryPlan(): QueryPlan // Get raw SQL with bindings interpolated->toSql(): string // Get column metadata analysis->getColumnsInfo(): array->getDatabaseColumns(): array->getDatabaseColumnsInfo(): array
Inline Editing
Three column types support inline editing — cells become editable inputs that validate and save immediately:
| Column Type | UI Element | Saves On |
|---|---|---|
TextInputColumn |
<input> |
Blur or Enter |
SelectColumn |
<select> |
Change |
ToggleColumn |
Switch | Click |
use NyonCode\WireTable\Columns\TextInputColumn;use NyonCode\WireTable\Columns\SelectColumn;use NyonCode\WireTable\Columns\ToggleColumn; $table->columns([ TextInputColumn::make('name') ->rules(['required', 'string', 'max:255']) ->saveOnBlur(), SelectColumn::make('status') ->options([ 'draft' => 'Draft', 'review' => 'In Review', 'published' => 'Published', ]) ->rules(['required', 'in:draft,review,published']), ToggleColumn::make('is_featured') ->onColor('success') ->offColor('gray') ->disabled(fn ($record) => ! $record->is_published),]);
Inline Edit Lifecycle
- User modifies cell value
updateCell($column, $recordKey, $newValue)is called- Validation runs against column rules
- Event
CellUpdatingdispatched (can be listened to) - Eloquent update persists the new value
- Event
CellUpdateddispatched - Success notification shown
If validation fails, the cell reverts and shows an error message.
Custom Save Logic
TextInputColumn::make('name') ->rules(['required', 'string', 'max:255']) ->editableUsing(function (Model $record, string $column, mixed $value) { // Custom save logic $record->update([$column => Str::title($value)]); Cache::forget("user:{$record->id}"); })
Real-World Patterns
Multi-Tenant Table
public function table(Table $table): Table{ return $table ->query( Order::query()->where('tenant_id', auth()->user()->tenant_id) ) ->columns([...]) ->filters([...]);}
Table with Complex Relations
$table->model(Invoice::class) ->columns([ TextColumn::make('number')->searchable(), TextColumn::make('client.company.name') // nested relation ->label('Company') ->searchable(), TextColumn::make('items.sum.amount') // aggregate ->label('Total') ->money('CZK'), TextColumn::make('payments.count') // count aggregate ->label('Payments'), BadgeColumn::make('status') ->colors([...]), ]);
Conditional Actions
$table->actions([ Action::make('approve') ->icon('check') ->color('success') ->visible(fn ($record) => $record->status === 'pending') ->action(fn ($record) => $record->approve()), Action::make('edit') ->icon('pencil') ->disabled(fn ($record) => $record->is_locked) ->url(fn ($record) => route('invoices.edit', $record)), ActionGroup::make('more', [ Action::make('duplicate') ->icon('copy') ->action(fn ($r) => $r->replicate()->save()), Action::make('pdf') ->icon('document') ->url(fn ($r) => route('invoices.pdf', $r)) ->openUrlInNewTab(), Action::divider(), Action::make('delete') ->icon('trash') ->color('danger') ->requiresConfirmation() ->modalHeading('Delete Invoice?') ->action(fn ($r) => $r->delete()), ]),]);
Dynamic Per-Page with URL Sync
All state properties are Livewire-bound, so they persist across page loads via query string (if configured in your Livewire component):
class UserTable extends Component{ use WithTable; // Persist state in URL protected $queryString = [ 'tableSearch' => ['except' => ''], 'tableSortColumn' => ['except' => ''], 'tableSortDirection' => ['except' => 'asc'], 'tablePerPage' => ['except' => 10], ];}
Related Documentation
| Document | What It Covers |
|---|---|
| Columns | All 13 column types — TextColumn, BadgeColumn, BooleanColumn, IconColumn, ImageColumn, ButtonColumn, ToggleColumn, SelectColumn, TextInputColumn, StackedColumn, SplitColumn, PollColumn |
| Filters | SelectFilter, DateFilter, NumberRangeFilter, TernaryFilter, custom filters, column-level filters |
| Exports | CSV, Excel, and PDF exports for the current table query |
| Advanced | Sub-rows, summary footer, polling, lazy loading, caching, debug, responsive |
| Actions | Full Action system — modals, forms, wizard steps, lifecycle |