Components

Data Table

An interactive table with search, sortable columns, row selection and pagination.

php artisan blatui:add data-table
Select
No results.

of row(s) selected.

Page of
@php($rows = [
    ['name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner', 'amount' => '$1,200'],
    ['name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member', 'amount' => '$840'],
    ['name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member', 'amount' => '$2,100'],
    ['name' => 'William Kim', 'email' => '[email protected]', 'role' => 'Admin', 'amount' => '$640'],
    ['name' => 'Sofia Davis', 'email' => '[email protected]', 'role' => 'Member', 'amount' => '$1,950'],
    ['name' => 'Liam Johnson', 'email' => '[email protected]', 'role' => 'Member', 'amount' => '$320'],
    ['name' => 'Emma Brown', 'email' => '[email protected]', 'role' => 'Admin', 'amount' => '$5,400'],
    ['name' => 'Noah Wilson', 'email' => '[email protected]', 'role' => 'Member', 'amount' => '$1,100'],
    ['name' => 'Ava Garcia', 'email' => '[email protected]', 'role' => 'Member', 'amount' => '$760'],
    ['name' => 'Mason Lopez', 'email' => '[email protected]', 'role' => 'Owner', 'amount' => '$3,300'],
    ['name' => 'Mia Hernandez', 'email' => '[email protected]', 'role' => 'Member', 'amount' => '$220'],
    ['name' => 'Lucas Young', 'email' => '[email protected]', 'role' => 'Member', 'amount' => '$1,480'],
])

<x-ui.data-table
    class="w-full max-w-2xl"
    :columns="[
        ['key' => 'name', 'label' => 'Name'],
        ['key' => 'email', 'label' => 'Email'],
        ['key' => 'role', 'label' => 'Role'],
        ['key' => 'amount', 'label' => 'Amount', 'class' => 'text-right'],
    ]"
    :rows="$rows"
    search-key="name"
    search-placeholder="Filter names..."
    :page-size="5"
/>

Full

Select Actions
No results.

of row(s) selected.

Page of
@php($rows = [
    ['id' => 1, 'name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner', 'amount' => '$1,200'],
    ['id' => 2, 'name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member', 'amount' => '$840'],
    ['id' => 3, 'name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member', 'amount' => '$2,100'],
    ['id' => 4, 'name' => 'William Kim', 'email' => '[email protected]', 'role' => 'Admin', 'amount' => '$640'],
    ['id' => 5, 'name' => 'Sofia Davis', 'email' => '[email protected]', 'role' => 'Member', 'amount' => '$1,950'],
    ['id' => 6, 'name' => 'Liam Johnson', 'email' => '[email protected]', 'role' => 'Member', 'amount' => '$320'],
])

{{-- Selection + search + sortable columns + row actions together — the full interactive table. --}}
<x-ui.data-table
    class="w-full max-w-2xl"
    row-key="id"
    search-placeholder="Filter members..."
    :columns="[
        ['key' => 'name', 'label' => 'Name'],
        ['key' => 'email', 'label' => 'Email'],
        ['key' => 'role', 'label' => 'Role'],
        ['key' => 'amount', 'label' => 'Amount', 'class' => 'text-right'],
    ]"
    :rows="$rows"
    :page-size="5"
>
    <x-slot:actions>
        <x-ui.button size="sm" variant="ghost" x-on:click="$wire.edit(item.r.id)" aria-label="Edit row">
            <x-lucide-pencil class="size-4" aria-hidden="true" />
            <span class="sr-only sm:not-sr-only">Edit</span>
        </x-ui.button>
        <x-ui.button size="sm" variant="ghost" class="text-destructive hover:text-destructive" x-on:click="$wire.delete(item.r.id)" aria-label="Delete row">
            <x-lucide-trash-2 class="size-4" aria-hidden="true" />
            <span class="sr-only sm:not-sr-only">Delete</span>
        </x-ui.button>
    </x-slot:actions>
</x-ui.data-table>

Global Search

No results.

result(s).

Page of
@php($rows = [
    ['name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner'],
    ['name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member'],
    ['name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member'],
    ['name' => 'William Kim', 'email' => '[email protected]', 'role' => 'Admin'],
    ['name' => 'Sofia Davis', 'email' => '[email protected]', 'role' => 'Member'],
])

{{--
    Omit search-key and the filter matches across every column — try typing "admin" or a name.
    Filtering runs entirely client-side in Alpine.
--}}
<x-ui.data-table
    class="w-full max-w-2xl"
    :selectable="false"
    search-placeholder="Search name, email or role..."
    :columns="[
        ['key' => 'name', 'label' => 'Name'],
        ['key' => 'email', 'label' => 'Email'],
        ['key' => 'role', 'label' => 'Role'],
    ]"
    :rows="$rows"
/>

No Selection

No results.

result(s).

Page of
@php($rows = [
    ['name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner', 'amount' => '$1,200'],
    ['name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member', 'amount' => '$840'],
    ['name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member', 'amount' => '$2,100'],
    ['name' => 'William Kim', 'email' => '[email protected]', 'role' => 'Admin', 'amount' => '$640'],
    ['name' => 'Sofia Davis', 'email' => '[email protected]', 'role' => 'Member', 'amount' => '$1,950'],
])

{{-- selectable="false" drops the checkbox column; the footer then reports the result count. --}}
<x-ui.data-table
    class="w-full max-w-2xl"
    :selectable="false"
    :columns="[
        ['key' => 'name', 'label' => 'Name'],
        ['key' => 'email', 'label' => 'Email'],
        ['key' => 'role', 'label' => 'Role'],
        ['key' => 'amount', 'label' => 'Amount', 'class' => 'text-right'],
    ]"
    :rows="$rows"
    :page-size="5"
/>

Row Actions

Actions
No results.

result(s).

Page of
@php($rows = [
    ['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'],
])

{{--
    The `actions` slot renders inside the client-side x-for, so the current row is `item.r`.
    Bind to Livewire with $wire (shown below). In this static preview there is no Livewire
    runtime, so the clicks are inert — copy this into a Livewire component to wire them up.
--}}
<x-ui.data-table
    class="w-full max-w-2xl"
    :columns="[
        ['key' => 'name', 'label' => 'Name'],
        ['key' => 'email', 'label' => 'Email'],
        ['key' => 'role', 'label' => 'Role'],
    ]"
    :rows="$rows"
    row-key="id"
    :selectable="false"
    :searchable="false"
    :page-size="5"
>
    <x-slot:actions>
        <x-ui.button size="sm" variant="ghost" x-on:click="$wire.edit(item.r.id)" aria-label="Edit row">
            <x-lucide-pencil class="size-4" aria-hidden="true" />
            <span class="sr-only sm:not-sr-only">Edit</span>
        </x-ui.button>
        <x-ui.button size="sm" variant="ghost" class="text-destructive hover:text-destructive" x-on:click="$wire.delete(item.r.id)" aria-label="Delete row">
            <x-lucide-trash-2 class="size-4" aria-hidden="true" />
            <span class="sr-only sm:not-sr-only">Delete</span>
        </x-ui.button>
    </x-slot:actions>
</x-ui.data-table>

Sticky Actions

Actions
No results.

result(s).

Page of
@php($rows = [
    ['id' => 1, 'name' => 'Olivia Martin', 'email' => '[email protected]', 'role' => 'Owner', 'team' => 'Design', 'location' => 'Lisbon'],
    ['id' => 2, 'name' => 'Jackson Lee', 'email' => '[email protected]', 'role' => 'Member', 'team' => 'Engineering', 'location' => 'Berlin'],
    ['id' => 3, 'name' => 'Isabella Nguyen', 'email' => '[email protected]', 'role' => 'Member', 'team' => 'Marketing', 'location' => 'Toronto'],
])

{{-- stickyActions freezes the actions column to the right edge while the table scrolls sideways. --}}
<x-ui.data-table
    class="w-full max-w-2xl"
    row-key="id"
    :selectable="false"
    :searchable="false"
    sticky-actions
    :columns="[
        ['key' => 'name', 'label' => 'Name'],
        ['key' => 'email', 'label' => 'Email'],
        ['key' => 'role', 'label' => 'Role'],
        ['key' => 'team', 'label' => 'Team'],
        ['key' => 'location', 'label' => 'Location'],
    ]"
    :rows="$rows"
>
    <x-slot:actions>
        <x-ui.button size="sm" variant="ghost" x-on:click="$wire.edit(item.r.id)" aria-label="Edit row">
            <x-lucide-pencil class="size-4" aria-hidden="true" />
        </x-ui.button>
        <x-ui.button size="sm" variant="ghost" class="text-destructive hover:text-destructive" x-on:click="$wire.delete(item.r.id)" aria-label="Delete row">
            <x-lucide-trash-2 class="size-4" aria-hidden="true" />
        </x-ui.button>
    </x-slot:actions>
</x-ui.data-table>

API Reference

Props, slots and exposed methods for <x-ui.data-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. See Each column below.
rows* array [] The data rows as an array of associative arrays. Each row is keyed by the column keys (e.g. ["name" => "...", "email" => "..."]). Filtering, sorting and pagination all run client-side in Alpine.
searchable bool true Show the search input above the table.
searchKey string Restrict searching to a single column key. When omitted, the search matches across all column keys.
searchPlaceholder string 'Search...' Placeholder text for the search input.
selectable bool true Show row checkboxes plus a select-all checkbox in the header, and report the selected count in the footer.
pageSize int 5 Number of rows shown per page.
rowKey string 'id' The row-data key used as a stable :key for each rendered row, and the identifier you read in the actions slot (item.r[rowKey]). Point it at your primary key so re-sorts and Livewire updates keep row identity.
actionsLabel string 'Actions' Accessible header label for the row-actions column. Rendered sr-only so the column reads correctly to screen readers without a visible header.
stickyActions bool false Freeze the actions column to the right edge so it stays visible while the table scrolls horizontally on small screens.

* Required.

Each column

Prop Type Default Description
key* string The row-data key this column reads its cell value from.
label string The header text. Defaults to the capitalized key.
sortable bool true Whether clicking the header sorts by this column (numeric when both values parse as numbers, otherwise locale string compare).
class string Extra CSS classes applied to this column's header and cells, e.g. "text-right".

Slots

Slot Description
actions Optional per-row action buttons. Rendered inside the Alpine x-for, so the current row is available as item.r (the row data) and item.i (its index). Bind to Livewire with $wire, e.g. x-on:click="$wire.edit(item.r.id)". Because rendering is client-side, use Alpine bindings here — not server-side {{ $row->id }} interpolation. For a fully server-rendered table where wire:click="edit($row->id)" works natively, use server-table.