Data Table
The single most-demanded surface in any app — orders, tickets, contacts, users,
records — is one thing: a sortable, searchable, bulk-selectable table. The
data-table wrapper handles the 80% case entirely client-side: hand it a rows
array and a columns definition and it sorts, searches, selects, toggles column
visibility, and switches density with no backend round-trip. For 10k+ rows you
drive the same UI from Livewire via the server contract.
Basic Usage
Define columns (each {key, label}) and pass rows. Headers are sortable by
default — click to sort, click again to reverse.
Status Badges
A cellType: 'badge' column renders a tinted intent pill — common status words
(paid, pending, failed, …) map to success / warning / danger
automatically.
Selection & Bulk Actions
Add selectable for per-row checkboxes plus a tri-state select-all. When rows
are selected, a bulk-action bar appears — fill it with the bulkActions slot.
Search, Density & Column Manager
searchable adds a client-side filter box, the density toggle switches row
padding, and column-manager lets the user show or hide columns.
Row Actions
The rowActions slot adds a trailing actions column. Compose a dropdown or
buttons; the slot renders once per row.
Server Mode (Large Datasets)
For datasets too large to ship to the browser, add the server flag and drive
sort / search / selection from Livewire. The component stops sorting and
filtering locally and instead emits sort-change, search-change, and
selection-change events for you to handle:
{{-- 1. Server mode: the component renders the current page; you re-query --}}
<x-wirekit::data-table
server
selectable
name="selectedIds"
:columns="$columns"
:rows="$this->rows()->items()"
x-on:sort-change="$wire.set('sort', $event.detail)"
x-on:search-change="$wire.set('search', $event.detail.value)"
/>
{{-- 2. Compose pagination below it --}}
{{ $this->rows()->links() }}
// 3. A reusable trait carries the server-side state contract
trait WithDataTable
{
public array $sort = ['key' => null, 'dir' => 'asc'];
public string $search = '';
public array $selectedIds = [];
public function rows(): \Illuminate\Contracts\Pagination\LengthAwarePaginator
{
return Order::query()
->when($this->search, fn ($q) => $q->where('ref', 'like', "%{$this->search}%"))
->when($this->sort['key'], fn ($q) => $q->orderBy($this->sort['key'], $this->sort['dir']))
->paginate(25);
}
}
The selection-change event (and the hidden input the name prop creates)
carry the selected id list, so wire:model="selectedIds" stays in sync for bulk
operations.
Column Definition
| Key | Required | Description |
|---|---|---|
key |
yes | The row field to read |
label |
yes | The header text |
sortable |
no | false disables sorting for the column |
align |
no | left (default) · center · right |
cellType |
no | text (default) · number (right tabular) · badge |
Props
| Prop | Type | Default | Description |
|---|---|---|---|
rows |
array | [] |
Row objects |
columns |
array | [] |
Column definitions (see above) |
rowKey |
string | 'id' |
Unique id field (selection + morph keying) |
selectable |
bool | false |
Per-row + select-all checkboxes |
searchable |
bool | false |
Toolbar search box |
density |
string | 'comfortable' |
comfortable · compact |
columnManager |
bool | false |
Show/hide-columns dropdown |
hidden |
array | [] |
Initially-hidden column keys |
server |
bool | false |
Server-driven — stop local sort/filter, emit events only |
searchPlaceholder |
string | 'Search…' |
Search box placeholder |
emptyText |
string | 'No results' |
Empty-state message |
caption |
string|null | null |
Accessible table caption / name |
name |
string|null | null |
Hidden-input name mirroring selected ids |
scope |
string|null | null |
Scoped personalization name |
Accessibility
- The table sits in a labeled
role="region"withtabindex="0"and a focus ring, so the horizontally-scrolling grid is keyboard-reachable (WCAG 2.1.1). - Sortable headers are buttons carrying
aria-sort(ascending/descending/none); the sort direction is shown with a caret glyph, not color. - The select-all checkbox is tri-state (
indeterminatewhen a subset is selected); the selection count is announced via anaria-liveregion. - A
captionbecomes the table's accessible name.
Keyboard Interaction
| Key | Action |
|---|---|
Tab |
Move through the toolbar, headers, checkboxes, and row actions |
Enter / Space |
Sort a column, toggle a checkbox, or trigger an action |
Design Tokens
| Element | Token |
|---|---|
| Header text | --color-wk-text-muted |
| Row hover | --color-wk-bg-subtle |
| Selected row | --color-wk-bg-muted |
| Bulk-action bar | --color-wk-bg-muted |
| Checkbox accent | --color-wk-accent |
| Badge (success/warning/danger) | --color-wk-success / --color-wk-warning / --color-wk-danger |
| Focus ring | --color-wk-ring |
Customization
Override the defaults via config/wirekit.php:
'components' => [
'data-table' => [
'density' => 'compact',
'selectable' => true,
'searchable' => true,
],
],