Components
Server Table
A server-rendered, Livewire-first data table with declarative row actions, server sorting, search, selection and pagination.
| Name | Role | |
|---|---|---|
| Olivia Martin | [email protected] | Owner |
| Jackson Lee | [email protected] | Member |
| Isabella Nguyen | [email protected] | Member |
| William Kim | [email protected] | Admin |
| Sofia Davis | [email protected] | Member |
@php($users = [
['id' => 1, 'name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner'],
['id' => 2, 'name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 3, 'name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 4, 'name' => 'William Kim', 'email' => '[email protected]', 'role' => 'Admin'],
['id' => 5, 'name' => 'Sofia Davis', 'email' => '[email protected]', 'role' => 'Member'],
])
{{-- Server-rendered: pass an array, an Eloquent collection, or a paginator as :rows. --}}
<x-ui.server-table
class="w-full max-w-2xl"
caption="Team members"
:columns="[
['key' => 'name', 'label' => 'Name'],
['key' => 'email', 'label' => 'Email'],
['key' => 'role', 'label' => 'Role', 'align' => 'right'],
]"
:rows="$users"
/>
Actions Dropdown
| Name | Role | Actions | |
|---|---|---|---|
| Olivia Martin | [email protected] | Owner |
|
| Jackson Lee | [email protected] | Member |
|
| Isabella Nguyen | [email protected] | Member |
|
| William Kim | [email protected] | Admin |
|
@php($users = [
['id' => 1, 'name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner'],
['id' => 2, 'name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 3, 'name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 4, 'name' => 'William Kim', 'email' => '[email protected]', 'role' => 'Admin'],
])
{{-- With many actions, collapse them into an overflow "…" menu — tidier on narrow screens. --}}
<x-ui.server-table
class="w-full max-w-2xl"
actions-mode="dropdown"
:columns="[
['key' => 'name', 'label' => 'Name'],
['key' => 'email', 'label' => 'Email'],
['key' => 'role', 'label' => 'Role'],
]"
:rows="$users"
:actions="[
['label' => 'View', 'icon' => 'eye', 'method' => 'view'],
['label' => 'Edit', 'icon' => 'pencil', 'method' => 'edit'],
['label' => 'Duplicate', 'icon' => 'copy', 'method' => 'duplicate'],
['label' => 'Delete', 'icon' => 'trash-2', 'method' => 'delete', 'variant' => 'destructive', 'confirm' => 'Delete this user?'],
]"
/>
Card
| Name | Role | Actions | |
|---|---|---|---|
| Olivia Martin | [email protected] | Owner |
|
| Jackson Lee | [email protected] | Member |
|
| Isabella Nguyen | [email protected] | Member |
|
@php($users = [
['id' => 1, 'name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner'],
['id' => 2, 'name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 3, 'name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member'],
])
{{-- variant="card" wraps the table in an elevated card surface. --}}
<x-ui.server-table
class="w-full max-w-2xl"
variant="card"
:columns="[
['key' => 'name', 'label' => 'Name'],
['key' => 'email', 'label' => 'Email'],
['key' => 'role', 'label' => 'Role'],
]"
:rows="$users"
:actions="[
['label' => 'Edit', 'icon' => 'pencil', 'method' => 'edit'],
]"
/>
Column Alignment
| Invoice | Client | Status | Amount |
|---|---|---|---|
| INV-001 | Acme Corp | Paid | $1,200.00 |
| INV-002 | Globex | Pending | $840.50 |
| INV-003 | Initech | Overdue | $2,100.00 |
@php($invoices = [
['id' => 'INV-001', 'client' => 'Acme Corp', 'status' => 'Paid', 'amount' => '$1,200.00'],
['id' => 'INV-002', 'client' => 'Globex', 'status' => 'Pending', 'amount' => '$840.50'],
['id' => 'INV-003', 'client' => 'Initech', 'status' => 'Overdue', 'amount' => '$2,100.00'],
])
{{--
Per-column `align` (left | center | right) and a fixed `width`. Numeric / money columns read
best right-aligned. rowKey points at a non-"id" primary key here ("id" is a string invoice no.).
--}}
<x-ui.server-table
class="w-full max-w-2xl"
row-key="id"
:columns="[
['key' => 'id', 'label' => 'Invoice', 'width' => '8rem'],
['key' => 'client', 'label' => 'Client'],
['key' => 'status', 'label' => 'Status', 'align' => 'center'],
['key' => 'amount', 'label' => 'Amount', 'align' => 'right'],
]"
:rows="$invoices"
/>
Conditional Actions
| Name | Role | Actions | |
|---|---|---|---|
| Olivia Martin | [email protected] | Owner |
|
| Jackson Lee | [email protected] | Member |
|
| Isabella Nguyen | [email protected] | Member |
|
| William Kim | [email protected] | Admin |
|
@php($users = [
['id' => 1, 'name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner'],
['id' => 2, 'name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 3, 'name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 4, 'name' => 'William Kim', 'email' => '[email protected]', 'role' => 'Admin'],
])
{{--
Show an action only for some rows with a `visible` predicate — fn ($row) => bool. Here Delete
is hidden for Owners, so the destructive control never appears where it shouldn't. Great for
per-row permissions (e.g. auth()->user()->can('delete', $row)).
--}}
<x-ui.server-table
class="w-full max-w-2xl"
:columns="[
['key' => 'name', 'label' => 'Name'],
['key' => 'email', 'label' => 'Email'],
['key' => 'role', 'label' => 'Role'],
]"
:rows="$users"
:actions="[
['label' => 'Edit', 'icon' => 'pencil', 'method' => 'edit'],
[
'label' => 'Delete',
'icon' => 'trash-2',
'method' => 'delete',
'class' => 'text-destructive hover:text-destructive',
'confirm' => 'Delete this user?',
'visible' => fn ($row) => $row['role'] !== 'Owner',
],
]"
/>
Custom Actions
| Name | Role | Actions | |
|---|---|---|---|
| Olivia Martin | [email protected] | Owner |
|
| Jackson Lee | [email protected] | Member |
|
| Isabella Nguyen | [email protected] | Member |
|
@php($users = [
['id' => 1, 'name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner'],
['id' => 2, 'name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 3, 'name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member'],
])
{{--
Full control over the action markup: point `actions-view` at a Blade partial. It is included
per row with the real $row model in scope, so you write ordinary wire:click="edit($row->id)"
— the server-rendered answer to the data-table actions slot. See the partial for the markup.
--}}
<x-ui.server-table
class="w-full max-w-2xl"
actions-view="examples.server-table.partials.user-actions"
:columns="[
['key' => 'name', 'label' => 'Name'],
['key' => 'email', 'label' => 'Email'],
['key' => 'role', 'label' => 'Role'],
]"
:rows="$users"
/>
Custom Cells
| Member | Role | Status | Actions |
|---|---|---|---|
|
Olivia Martin
[email protected]
|
Owner | Active |
|
|
Jackson Lee
[email protected]
|
Member | Invited |
|
|
Isabella Nguyen
[email protected]
|
Member | Suspended |
|
|
William Kim
[email protected]
|
Admin | Active |
|
@php($users = [
['id' => 1, 'name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner', 'status' => 'Active'],
['id' => 2, 'name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member', 'status' => 'Invited'],
['id' => 3, 'name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member', 'status' => 'Suspended'],
['id' => 4, 'name' => 'William Kim', 'email' => '[email protected]', 'role' => 'Admin', 'status' => 'Active'],
])
{{--
Render any column with your own Blade via `cell-views`: a map of column key => view. Each view
is included with $value (the cell value) and $row (the model) in scope — here a rich name/avatar
cell and a status badge. Perfect for badges, avatars, links, money and dates.
--}}
<x-ui.server-table
class="w-full max-w-2xl"
:columns="[
['key' => 'name', 'label' => 'Member'],
['key' => 'role', 'label' => 'Role'],
['key' => 'status', 'label' => 'Status'],
]"
:rows="$users"
:cell-views="[
'name' => 'examples.server-table.partials.user-cell',
'status' => 'examples.server-table.partials.status',
]"
:actions="[
['label' => 'Edit', 'icon' => 'pencil', 'method' => 'edit'],
]"
/>
Empty
| Name | Role | Actions | |
|---|---|---|---|
|
No members match your filters.
|
|||
{{-- When :rows is empty, server-table shows a centered empty state. Customize emptyText / emptyIcon. --}}
<x-ui.server-table
class="w-full max-w-2xl"
:columns="[
['key' => 'name', 'label' => 'Name'],
['key' => 'email', 'label' => 'Email'],
['key' => 'role', 'label' => 'Role'],
]"
:rows="[]"
empty-text="No members match your filters."
empty-icon="users"
:actions="[
['label' => 'Edit', 'icon' => 'pencil', 'method' => 'edit'],
]"
/>
Kitchen Sink
| Select | Status | Actions | ||
|---|---|---|---|---|
|
Olivia Martin
[email protected]
|
Owner | Active | ||
|
Jackson Lee
[email protected]
|
Member | Invited | ||
|
Isabella Nguyen
[email protected]
|
Member | Suspended | ||
|
William Kim
[email protected]
|
Admin | Active |
@php($users = [
['id' => 1, 'name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner', 'status' => 'Active'],
['id' => 2, 'name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member', 'status' => 'Invited'],
['id' => 3, 'name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member', 'status' => 'Suspended'],
['id' => 4, 'name' => 'William Kim', 'email' => '[email protected]', 'role' => 'Admin', 'status' => 'Active'],
])
{{--
Everything at once: a card surface, search, row selection, sortable columns, a custom status
cell, and a dropdown of row actions (with a permission-gated Delete). This is the shape of a
real CRUD admin table — drop it into a Livewire component and wire the methods.
--}}
<x-ui.server-table
class="w-full max-w-3xl"
variant="card"
searchable
selectable
actions-mode="dropdown"
sort="name"
direction="asc"
caption="Team members"
:columns="[
['key' => 'name', 'label' => 'Member', 'sortable' => true],
['key' => 'role', 'label' => 'Role', 'sortable' => true],
['key' => 'status', 'label' => 'Status', 'align' => 'center'],
]"
:rows="$users"
:cell-views="[
'name' => 'examples.server-table.partials.user-cell',
'status' => 'examples.server-table.partials.status',
]"
:actions="[
['label' => 'View', 'icon' => 'eye', 'href' => '/users/{id}'],
['label' => 'Edit', 'icon' => 'pencil', 'method' => 'edit'],
['label' => 'Duplicate', 'icon' => 'copy', 'method' => 'duplicate'],
['label' => 'Delete', 'icon' => 'trash-2', 'method' => 'delete', 'variant' => 'destructive', 'confirm' => 'Delete this user?', 'visible' => fn ($row) => $row['role'] !== 'Owner'],
]"
/>
Link Actions
| Name | Role | Actions | |
|---|---|---|---|
| Olivia Martin | [email protected] | Owner | |
| Jackson Lee | [email protected] | Member | |
| Isabella Nguyen | [email protected] | Member |
@php($users = [
['id' => 1, 'name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner'],
['id' => 2, 'name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 3, 'name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member'],
])
{{--
Actions can navigate instead of calling a method. Use `href` — a string with {id} substituted,
or a closure fn ($row) => route(...). `iconOnly` keeps the button compact (label stays as an
aria-label). Here: a link View, an icon-only Edit, and a method-based Delete.
--}}
<x-ui.server-table
class="w-full max-w-2xl"
:columns="[
['key' => 'name', 'label' => 'Name'],
['key' => 'email', 'label' => 'Email'],
['key' => 'role', 'label' => 'Role'],
]"
:rows="$users"
:actions="[
['label' => 'View', 'icon' => 'external-link', 'href' => '/users/{id}'],
['label' => 'Edit', 'icon' => 'pencil', 'method' => 'edit', 'iconOnly' => true],
['label' => 'Delete', 'icon' => 'trash-2', 'method' => 'delete', 'iconOnly' => true, 'class' => 'text-destructive hover:text-destructive', 'confirm' => 'Delete this user?'],
]"
/>
Pagination
| Name | Role | |
|---|---|---|
| Olivia Martin | [email protected] | Owner |
| Jackson Lee | [email protected] | Member |
| Isabella Nguyen | [email protected] | Member |
| William Kim | [email protected] | Admin |
| Sofia Davis | [email protected] | Member |
@php
$all = collect([
['id' => 1, 'name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner'],
['id' => 2, 'name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 3, 'name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 4, 'name' => 'William Kim', 'email' => '[email protected]', 'role' => 'Admin'],
['id' => 5, 'name' => 'Sofia Davis', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 6, 'name' => 'Liam Johnson', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 7, 'name' => 'Emma Brown', 'email' => '[email protected]', 'role' => 'Admin'],
['id' => 8, 'name' => 'Noah Wilson', 'email' => '[email protected]', 'role' => 'Member'],
]);
$perPage = 5;
$page = 1;
// In a real app this is simply User::paginate(5) inside a Livewire component (WithPagination).
$users = new \Illuminate\Pagination\LengthAwarePaginator(
$all->forPage($page, $perPage)->values(),
$all->count(),
$perPage,
$page,
['path' => '#'],
);
@endphp
{{-- Pass a paginator as :rows and server-table renders its page links below the table. --}}
<x-ui.server-table
class="w-full max-w-2xl"
:columns="[
['key' => 'name', 'label' => 'Name'],
['key' => 'email', 'label' => 'Email'],
['key' => 'role', 'label' => 'Role'],
]"
:rows="$users"
/>
Responsive Stack
| Name | Role | Actions | |
|---|---|---|---|
| Olivia Martin | [email protected] | Owner |
|
| Jackson Lee | [email protected] | Member |
|
| Isabella Nguyen | [email protected] | Member |
|
@php($users = [
['id' => 1, 'name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner'],
['id' => 2, 'name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 3, 'name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member'],
])
{{--
responsive="stack" turns each row into a labelled card below the md breakpoint (resize the
preview to see it), while staying a normal table on larger screens. Each cell keeps a visible,
screen-reader-friendly label on mobile — no reliance on CSS pseudo-content.
--}}
<x-ui.server-table
class="w-full max-w-2xl"
responsive="stack"
variant="card"
:columns="[
['key' => 'name', 'label' => 'Name'],
['key' => 'email', 'label' => 'Email'],
['key' => 'role', 'label' => 'Role'],
]"
:rows="$users"
:actions="[
['label' => 'Edit', 'icon' => 'pencil', 'method' => 'edit'],
['label' => 'Delete', 'icon' => 'trash-2', 'method' => 'delete', 'class' => 'text-destructive hover:text-destructive', 'confirm' => 'Delete this user?'],
]"
/>
Row Actions
| Name | Role | Actions | |
|---|---|---|---|
| Olivia Martin | [email protected] | Owner |
|
| Jackson Lee | [email protected] | Member |
|
| Isabella Nguyen | [email protected] | Member |
|
| William Kim | [email protected] | Admin |
|
@php($users = [
['id' => 1, 'name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner'],
['id' => 2, 'name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 3, 'name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 4, 'name' => 'William Kim', 'email' => '[email protected]', 'role' => 'Admin'],
])
{{--
Declarative row actions. Each renders server-side with a native wire:click carrying the real
primary key — wire:click="edit(1)", wire:click="delete(1)" — so no client-side plumbing is
needed. `confirm` adds a wire:confirm prompt before the action runs.
--}}
<x-ui.server-table
class="w-full max-w-2xl"
:columns="[
['key' => 'name', 'label' => 'Name'],
['key' => 'email', 'label' => 'Email'],
['key' => 'role', 'label' => 'Role'],
]"
:rows="$users"
:actions="[
['label' => 'Edit', 'icon' => 'pencil', 'method' => 'edit'],
['label' => 'Delete', 'icon' => 'trash-2', 'method' => 'delete', 'class' => 'text-destructive hover:text-destructive', 'confirm' => 'Delete this user?'],
]"
/>
Searchable
| Name | Role | |
|---|---|---|
| Olivia Martin | [email protected] | Owner |
| Jackson Lee | [email protected] | Member |
| Isabella Nguyen | [email protected] | Member |
@php($users = [
['id' => 1, 'name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner'],
['id' => 2, 'name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 3, 'name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member'],
])
{{--
A search input bound to a Livewire property (search-model="search"). You run the actual
filtering in your query: ->when($this->search, fn ($q) => $q->where('name', 'like', "%{$this->search}%")).
The input is inert in this static preview.
--}}
<x-ui.server-table
class="w-full max-w-2xl"
searchable
search-model="search"
search-placeholder="Search members..."
:columns="[
['key' => 'name', 'label' => 'Name'],
['key' => 'email', 'label' => 'Email'],
['key' => 'role', 'label' => 'Role'],
]"
:rows="$users"
/>
Selection
| Select | Name | Role | |
|---|---|---|---|
| Olivia Martin | [email protected] | Owner | |
| Jackson Lee | [email protected] | Member | |
| Isabella Nguyen | [email protected] | Member | |
| William Kim | [email protected] | Admin |
@php($users = [
['id' => 1, 'name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner'],
['id' => 2, 'name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 3, 'name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 4, 'name' => 'William Kim', 'email' => '[email protected]', 'role' => 'Admin'],
])
{{--
Row selection: checkboxes bind to a Livewire array via wire:model (select-model="selected"),
and the header checkbox toggles every row on the page. Pair it with bulk actions in your
component. The select-all checkbox works client-side here so you can try it in this preview.
--}}
<x-ui.server-table
class="w-full max-w-2xl"
selectable
select-model="selected"
:columns="[
['key' => 'name', 'label' => 'Name'],
['key' => 'email', 'label' => 'Email'],
['key' => 'role', 'label' => 'Role'],
]"
:rows="$users"
/>
Sortable
| Role | ||
|---|---|---|
| Olivia Martin | [email protected] | Owner |
| Isabella Nguyen | [email protected] | Member |
| Jackson Lee | [email protected] | Member |
| Sofia Davis | [email protected] | Member |
| William Kim | [email protected] | Admin |
@php($users = [
['id' => 1, 'name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner'],
['id' => 2, 'name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 3, 'name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 4, 'name' => 'Sofia Davis', 'email' => '[email protected]', 'role' => 'Member'],
['id' => 5, 'name' => 'William Kim', 'email' => '[email protected]', 'role' => 'Admin'],
])
{{--
Sortable headers emit wire:click="sortBy('key')". Bind :sort / :direction to Livewire props
and order your query there — the header chevron and aria-sort follow the current state.
Here they are set statically to show the active "Name ▲" state.
--}}
<x-ui.server-table
class="w-full max-w-2xl"
:columns="[
['key' => 'name', 'label' => 'Name', 'sortable' => true],
['key' => 'email', 'label' => 'Email', 'sortable' => true],
['key' => 'role', 'label' => 'Role'],
]"
:rows="$users"
sort="name"
direction="asc"
sort-method="sortBy"
/>
Sticky Actions
| Name | Role | Team | Location | Joined | Actions | |
|---|---|---|---|---|---|---|
| Olivia Martin | [email protected] | Owner | Design | Lisbon | 2023-01-12 |
|
| Jackson Lee | [email protected] | Member | Engineering | Berlin | 2023-03-04 |
|
| Isabella Nguyen | [email protected] | Member | Marketing | Toronto | 2022-11-21 |
|
@php($users = [
['id' => 1, 'name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner', 'team' => 'Design', 'location' => 'Lisbon', 'joined' => '2023-01-12'],
['id' => 2, 'name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member', 'team' => 'Engineering', 'location' => 'Berlin', 'joined' => '2023-03-04'],
['id' => 3, 'name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member', 'team' => 'Marketing', 'location' => 'Toronto', 'joined' => '2022-11-21'],
])
{{--
With many columns the table scrolls horizontally on narrow screens. stickyActions freezes the
actions column to the right edge so the controls stay reachable. Scroll the table sideways.
--}}
<x-ui.server-table
class="w-full max-w-2xl"
sticky-actions
:columns="[
['key' => 'name', 'label' => 'Name'],
['key' => 'email', 'label' => 'Email'],
['key' => 'role', 'label' => 'Role'],
['key' => 'team', 'label' => 'Team'],
['key' => 'location', 'label' => 'Location'],
['key' => 'joined', 'label' => 'Joined'],
]"
:rows="$users"
:actions="[
['label' => 'Edit', 'icon' => 'pencil', 'method' => 'edit'],
['label' => 'Delete', 'icon' => 'trash-2', 'method' => 'delete', 'class' => 'text-destructive hover:text-destructive', 'confirm' => 'Delete this user?'],
]"
/>
Using with Livewire
wire:model
The examples above are the frontend (Blade + Alpine) usage. Inside a Livewire component, bind
wire:model for two-way
state — same component, no wrappers.
Full Livewire guide →
use Livewire\Component;
class Demo extends Component
{
use \Livewire\WithPagination;
public ?string $search = null;
public string $sort = 'name';
public string $direction = 'asc';
public array $selected = [];
#[\Livewire\Attributes\Computed]
public function users()
{
return \App\Models\User::query()
->when($this->search, fn ($q) => $q->where('name', 'like', "%{$this->search}%"))
->orderBy($this->sort, $this->direction)
->paginate(10);
}
public function sortBy(string $key): void
{
$this->direction = $this->sort === $key && $this->direction === 'asc' ? 'desc' : 'asc';
$this->sort = $key;
$this->resetPage();
}
public function delete(int $id): void
{
\App\Models\User::findOrFail($id)->delete();
}
public function render()
{
return view('livewire.demo');
}
}
<x-ui.server-table
:rows="$this->users"
row-key="id"
searchable
search-model="search"
:sort="$sort"
:direction="$direction"
sort-method="sortBy"
selectable
select-model="selected"
:columns="[
['key' => 'name', 'label' => 'Name', 'sortable' => true],
['key' => 'email', 'label' => 'Email', 'sortable' => true],
['key' => 'role', 'label' => 'Role'],
]"
:actions="[
['label' => 'Edit', 'icon' => 'pencil', 'method' => 'edit'],
['label' => 'Delete', 'icon' => 'trash-2', 'method' => 'delete', 'color' => 'var(--destructive)', 'confirm' => 'Delete this user?'],
]"
/>
Rows render server-side, so wire:click actions carry the real primary key and sorting/search/pagination run in your query — not client-side.
API Reference
Props, slots and exposed methods for
<x-ui.server-table>.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
columns* |
array
|
[]
|
The column definitions. The order here sets the column order, and each column reads its value from the matching key in every row (via data_get, so nested keys like "author.name" and Eloquent accessors both work). See Each column below. |
rows* |
array|Collection|Paginator
|
[]
|
The data rows. Accepts an array, an Eloquent collection, or a LengthAwarePaginator. Rows are rendered server-side with a real @foreach, so row-action buttons receive the genuine model and primary key. Sorting, searching and pagination are driven by your Livewire component, not client-side. |
rowKey |
string
|
'id'
|
The primary-key path read from each row (via data_get). Used for wire:key row identity and as the default argument passed to row-action methods. |
sort |
string
|
— | The currently sorted column key. Drives the active header state and the aria-sort attribute. Bind it to a Livewire property. |
direction |
string
|
'asc'
|
The current sort direction, reflected in the header chevron and aria-sort.
asc
desc
|
sortMethod |
string
|
'sortBy'
|
The Livewire method invoked when a sortable header is clicked, as wire:click="sortBy('key')". Implement it to flip direction and set the sort property. |
actions |
array
|
[]
|
Declarative per-row actions. Each renders a button (or menu item, in dropdown mode) with a native wire:click carrying the real primary key. See Each action below. |
actionsView |
string
|
— | Escape hatch for fully custom actions: a Blade view name included once per row with $row (the real model) in scope. Use raw wire:click="edit($row->id)" markup inside. Overrides the declarative actions array. |
actionsMode |
string
|
'inline'
|
Render declarative actions as inline buttons, or collapsed into an overflow "…" dropdown menu (better on narrow screens or with many actions).
inline
dropdown
|
actionsLabel |
string
|
'Actions'
|
Accessible label for the actions column header (rendered sr-only) and the dropdown trigger. |
stickyActions |
bool
|
false
|
Freeze the actions column to the right edge so it stays visible while the table scrolls horizontally. |
cellViews |
array
|
[]
|
Map of column key => Blade view name for custom cell rendering. Each view is included with $value (the cell value) and $row (the model) in scope, e.g. ["status" => "partials.status-badge"]. |
selectable |
bool
|
false
|
Show a checkbox column bound with wire:model to a Livewire array property, plus a select-all checkbox for the current page. |
selectModel |
string
|
'selected'
|
The Livewire array property the row checkboxes bind to via wire:model.live. |
searchable |
bool
|
false
|
Show a search input above the table, bound to a Livewire property. You perform the actual filtering in your query. |
searchModel |
string
|
'search'
|
The Livewire property the search input binds to via wire:model.live.debounce.300ms. |
searchPlaceholder |
string
|
'Search...'
|
Placeholder (and accessible label) for the search input. |
caption |
string
|
— | An accessible <caption> describing the table. Rendered sr-only unless captionVisible is true. |
captionVisible |
bool
|
false
|
Show the caption visibly above the table instead of only to screen readers. |
emptyText |
string
|
'No results.'
|
Message shown when rows is empty. |
emptyIcon |
string
|
'search-x'
|
Lucide icon name shown in the empty state. |
responsive |
string
|
'scroll'
|
How the table adapts on small screens. "scroll" keeps the table layout and scrolls horizontally; "stack" turns each row into a labelled card below the md breakpoint.
scroll
stack
|
variant |
string
|
'default'
|
Visual container style — a plain bordered table, or an elevated card surface.
default
card
|
paginate |
bool
|
true
|
When rows is a paginator, render its links() below the table (Livewire-aware when the WithPagination trait is used). |
* Required.
Each column
| Prop | Type | Default | Description |
|---|---|---|---|
key* |
string
|
— | The row-data path this column reads (data_get: supports "name", nested "author.name", and model accessors). |
label |
string
|
— | Header text. Defaults to the humanized key. |
sortable |
bool
|
false
|
Render the header as a sort button (wire:click to sortMethod). You perform the ordering server-side. |
align |
string
|
'left'
|
Text alignment for the header and cells.
left
center
right
|
width |
string
|
— | Optional fixed column width, e.g. "12rem" or "20%". |
class |
string
|
— | Extra CSS classes applied to this column's header and cells. |
Each action
| Prop | Type | Default | Description |
|---|---|---|---|
label* |
string
|
— | Button / menu-item text. Also used as the accessible label when iconOnly is set. |
method |
string
|
— | The Livewire method to call, rendered as wire:click="method(rowKey)". Omit when using href. |
href |
string|Closure
|
— | Render a link instead of a wire:click. A string may contain {id} (replaced with the row key), or pass a closure fn ($row) => route(...). |
params |
array
|
— | Override the method arguments. Defaults to [rowKey value]. Integers are passed bare, strings are quoted. |
icon |
string
|
— | Optional Lucide icon name shown before the label. |
iconOnly |
bool
|
false
|
Hide the label visually (kept as an aria-label). Inline mode only. |
variant |
string
|
'ghost'
|
Button variant (default, ghost, outline, secondary, destructive, link). Use "destructive" for delete-style actions. |
color |
string
|
— | Any CSS colour to recolor the button locally (best with solid variants), e.g. "var(--destructive)". |
class |
string
|
— | Extra CSS classes merged onto the button — e.g. "text-destructive hover:text-destructive" for a red ghost delete. Inline mode only. |
size |
string
|
'sm'
|
Button size (xs, sm, default, lg). Inline mode only. |
confirm |
string
|
— | A confirmation message. Rendered as wire:confirm, so Livewire prompts before running the action. |
visible |
Closure
|
— | Optional predicate fn ($row) => bool. Return false to hide this action for a given row (e.g. permissions). |
Slots
| Slot | Description |
|---|---|
actions (via actionsView) |
For custom action markup, pass actionsView="your.view"; it is included per row with the real $row model in scope, so wire:click="edit($row->id)" works natively. This is the server-rendered counterpart to the data-table actions slot. |