Components

Server Table

A server-rendered, Livewire-first data table with declarative row actions, server sorting, search, selection and pagination.

php artisan blatui:add server-table
Team members
Name Email 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 Email 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 Email 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 Email 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 Email 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 Email 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

Team members
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 Email 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 Email 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 Email 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 Email 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 Email 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 Email 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 Email 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 →

app/Livewire/Demo.php
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');
    }
}
resources/views/livewire/demo.blade.php
<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.