TallStackUI 4.0 is here! Seven new components and thousand of improvements. See what's new .

Powerful suite of Blade components for TALL Stack apps

Table

Table component.

You have two ways to provide data to create a table: 1) Simple data through an array or 2) Data that comes from the database, using Laravel Eloquent Pagination. The main difference between the two ways is that when choosing to create a table with an array of data, features such as filtering, sorting and pagination will be more difficult to implement.

1) Simple data through an array

@php
$headers = [
['index' => 'id', 'label' => '#'],
['index' => 'name', 'label' => 'Member'],
// ...
];
 
$rows = [
['id' => 1, 'name' => 'Taylor Otwell'],
['id' => 2, 'name' => 'Nuno Maduro'],
// ...
];
@endphp
 
<x-table :$headers :$rows />

2) Data that comes from the database

<?php
 
use App\Models\User;
use Livewire\Volt\Component;
 
new class extends Component {
 
public function with(): array
{
return [
'headers' => [
['index' => 'id', 'label' => '#'],
['index' => 'name', 'label' => 'Member'],
],
'rows' => User::all(),
];
}
}; ?>
 
<div>
<x-table :$headers :$rows />
</div>
# Member Name
1 Alice Morgan
2 Bruno Fernandes
3 Clara Dubois
4 Daniel Kowalski
5 Elena Rossi
6 Felix Andersen
7 Grace Nakamura
8 Hugo Martinez
9 Isla Petrov
10 Jonas Weber
11 Kira Svensson
<?php
 
use App\Models\User;
use Livewire\Volt\Component;
 
new class extends Component {
 
public function with(): array
{
return [
'headers' => [
['index' => 'id', 'label' => '#'],
['index' => 'name', 'label' => 'Member'],
// You can use `unescaped` to render raw HTML //
['index' => 'role', 'label' => '<b>Role</b>', 'unescaped' => true],
],
'rows' => User::all(),
];
}
}; ?>
 
<div>
<x-table :$headers :$rows />
</div>

An option to center or right-align a column through the header.

Product Quantity Price
Laravel 12 $99.00
Livewire 8 $49.00
Alpine 21 $0.00
<x-table :headers="[
['index' => 'name', 'label' => 'Product'],
['index' => 'quantity', 'label' => 'Quantity', 'align' => 'center'],
['index' => 'price', 'label' => 'Price', 'align' => 'right'],
]" :rows="[
['name' => 'Laravel', 'quantity' => 12, 'price' => '$99.00'],
['name' => 'Livewire', 'quantity' => 8, 'price' => '$49.00'],
['name' => 'Alpine', 'quantity' => 21, 'price' => '$0.00'],
]" />

Each header takes an align key: left by default, center or right . It reaches the header cell and every body cell of the column, including cells rendered through @interact . The sort icon follows the label. Any other value throws.

text-left moved out of table.th and table.th-compact into table.align.left . A customization that replaced text-left on those blocks should target table.align.left instead.
1 Alice Morgan
2 Bruno Fernandes
3 Clara Dubois
4 Daniel Kowalski
5 Elena Rossi
6 Felix Andersen
7 Grace Nakamura
8 Hugo Martinez
9 Isla Petrov
10 Jonas Weber
11 Kira Svensson
<?php
 
use App\Models\User;
use Livewire\Volt\Component;
 
new class extends Component {
 
public function with(): array
{
return [
'headers' => [
['index' => 'id', 'label' => '#'],
['index' => 'name', 'label' => 'Member'],
],
'rows' => User::all(),
];
}
}; ?>
 
<div>
<x-table :$headers :$rows headerless />
</div>
# Member Name
1 Alice Morgan
2 Bruno Fernandes
3 Clara Dubois
4 Daniel Kowalski
5 Elena Rossi
6 Felix Andersen
7 Grace Nakamura
8 Hugo Martinez
9 Isla Petrov
10 Jonas Weber
11 Kira Svensson
<?php
 
use App\Models\User;
use Livewire\Volt\Component;
 
new class extends Component {
 
public function with(): array
{
return [
'headers' => [
['index' => 'id', 'label' => '#'],
['index' => 'name', 'label' => 'Member'],
],
'rows' => User::all(),
];
}
}; ?>
 
<div>
<x-table :$headers :$rows striped />
</div>
# Member Name
1 Alice Morgan
2 Bruno Fernandes
<?php
 
use App\Models\User;
use Livewire\Volt\Component;
use Illuminate\Database\Eloquent\Builder;
 
new class extends Component {
 
public ?int $quantity = 2;
 
public ?string $search = null;
 
public function with(): array
{
return [
'headers' => [
['index' => 'id', 'label' => '#'],
['index' => 'name', 'label' => 'Member'],
],
'rows' => User::query()
->when($this->search, function (Builder $query) {
return $query->where('name', 'like', "%{$this->search}%");
})
->paginate($this->quantity)
->withQueryString()
];
}
}; ?>
 
<div>
<x-table :$headers :$rows filter />
 
<!-- You can control the items of the quantity selector -->
<x-table :$headers
:$rows
filter
:quantity="[2,5,10]" />
 
<!-- You can specify different properties for the filter -->
<x-table :$headers
:$rows
:filter="['quantity' => 'foo', 'search' => 'bar']"
:quantity="[2,5,10]" />
 
<!-- You can disable one of the filters -->
<x-table :$headers
:$rows
:filter="['quantity' => 'quantity']"
:quantity="[2,5,10]" />
</div>
The search input bind the property using wire:model.live with debounce of 500ms.

An option to display a loading effect when interacts with the table elements.

# Member Name
1 Alice Morgan
2 Bruno Fernandes
<?php
 
use App\Models\User;
use Livewire\Volt\Component;
use Illuminate\Database\Eloquent\Builder;
 
new class extends Component {
 
public ?int $quantity = 10;
 
public ?string $search = null;
 
public function with(): array
{
return [
'headers' => [
['index' => 'id', 'label' => '#'],
['index' => 'name', 'label' => 'Member'],
],
'rows' => User::query()
->when($this->search, function (Builder $query) {
return $query->where('name', 'like', "%{$this->search}%");
})
->paginate($this->quantity)
->withQueryString()
];
}
}; ?>
 
<div>
<x-table :$headers :$rows filter loading />
</div>

You can change the loading indicator icon to use a spinner new component in configuration file.

Sorting when clicking on the header names of the table.

# Member Name
20 Tara Johansson
19 Samuel Leclerc
18 Rosa Kim
17 Quinn O'Brien
16 Paul Müller
15 Olivia Santos
14 Noah Fischer
13 Maya Singh
12 Leo Chang
11 Kira Svensson
10 Jonas Weber
<?php
 
use App\Models\User;
use Livewire\Volt\Component;
 
new class extends Component {
 
public array $sort = [
'column' => 'id',
'direction' => 'desc',
];
 
public function with(): array
{
return [
'headers' => [
['index' => 'id', 'label' => '#'],
// You can disable the sorting for specific columns
['index' => 'name', 'label' => 'Member', 'sortable' => false],
],
'rows' => User::query()
->orderBy(...array_values($this->sort))
->paginate(10)
->withQueryString()
];
}
}; ?>
 
<div>
<x-table :$headers :$rows :$sort />
</div>
# Member Name
1 Alice Morgan
2 Bruno Fernandes
<?php
 
use App\Models\User;
use Livewire\WithPagination;
use Livewire\Volt\Component;
 
new class extends Component {
use WithPagination;
 
public function with(): array
{
return [
'headers' => [
['index' => 'id', 'label' => '#'],
['index' => 'name', 'label' => 'Member'],
],
'rows' => User::query()
->paginate(2)
->withQueryString()
];
}
}; ?>
 
<div>
<x-table :$headers :$rows paginate />
 
<!-- You can use the paginator with mobile style even when in desktop -->
<x-table :$headers :$rows paginate simple-pagination />
 
<!--
You can enable view persistence for the table by setting the `persistent`
-->
<x-table :$headers :$rows paginate persistent />
 
<!--
You can disable the TallStackUI paginator element. When you do that
the table component will use the Livewire paginator element.
-->
<x-table :$headers :$rows paginate :paginator="null" />
</div>

Header Slot

# Member Name
1 Alice Morgan
2 Bruno Fernandes

Footer Slot

<!-- This is a resumed example without the full explanation -->
 
<x-table header="Header Slot"
footer="Footer Slot"
... />
 
<!-- Or -->
 
<x-table ...>
<x-slot:header>
Raw Header Slot
</x-slot:header>
<x-slot:footer>
Raw Footer Slot
</x-slot:footer>
</x-table>
# Member Name
1 Alice Morgan
2 Bruno Fernandes
3 Clara Dubois
4 Daniel Kowalski
5 Elena Rossi
6 Felix Andersen
7 Grace Nakamura
8 Hugo Martinez
9 Isla Petrov
10 Jonas Weber
11 Kira Svensson
<!-- This is a resumed example without the full explanation -->
 
<!-- You need to create a public array property in the component to store
the selected rows. In this example we are using the `selected` property,
but you can choose any name, as long as it is an array. -->
 
<x-table ... selectable wire:model="selected" />
# Member Name
1 Alice Morgan
2 Bruno Fernandes
3 Clara Dubois
4 Daniel Kowalski
5 Elena Rossi
6 Felix Andersen
7 Grace Nakamura
8 Hugo Martinez
9 Isla Petrov
10 Jonas Weber
11 Kira Svensson
<!-- This is a resumed example without the full explanation -->
 
<x-table ... link="https://google.com.br/?user={id}" />
 
<x-table ... link="https://google.com.br/?user={name}" />
 
<!-- Using dot notation to use relationship data: -->
<x-table ... link="https://google.com.br/?postcode={address.postcode}" />
 
<!-- You can use blank to open the link in a new tab -->
<x-table ... link="https://google.com.br/?user={id}" blank />

An option to display a custom message when the table is empty.

# Member Name
No records found.
<!-- Using the attribute -->
<x-table :$headers :$rows empty="No records found." />
 
<!-- Using the slot for custom HTML -->
<x-table :$headers :$rows>
<x-slot:empty>
<p>No records found. <a href="/create" class="underline">Create one?</a></p>
</x-slot:empty>
</x-table>

An option to highlight rows based on a color property.

# Member Name
1 Alice Morgan
2 Bruno Fernandes
3 Clara Dubois
4 Daniel Kowalski
5 Elena Rossi
6 Felix Andersen
7 Grace Nakamura
8 Hugo Martinez
9 Isla Petrov
10 Jonas Weber
11 Kira Svensson
<?php
 
use App\Models\User;
use Livewire\Volt\Component;
 
new class extends Component {
 
public function with(): array
{
return [
'headers' => [
['index' => 'id', 'label' => '#'],
['index' => 'name', 'label' => 'Member'],
],
'rows' => User::all()->map(fn (User $user) => $user->setAttribute(
// Default property is `highlight`, but you can specify a
// custom property name with `highlight-property` attribute
'highlight', match ($user->id) {
1, 5 => 'green',
3 => 'red',
default => null,
}
)),
];
}
}; ?>
 
<div>
<x-table :$headers :$rows highlight />
</div>

The table component provides a custom Blade directive @interact to allow you to interact with the table columns about the data provided in each row. Allowing you to interact with the table and make things like add an action button for each row.

<?php
 
use App\Models\User;
use Livewire\WithPagination;
use Livewire\Volt\Component;
use Illuminate\Database\Eloquent\Builder;
 
new class extends Component {
use WithPagination;
 
public ?int $quantity = 10;
 
public ?string $search = null;
 
public function delete(string $id): void
{
dd($id);
}
 
public function with(): array
{
return [
'headers' => [
['index' => 'id', 'label' => '#'],
['index' => 'name', 'label' => 'Member'],
['index' => 'action'],
],
'rows' => User::query()
->when($this->search, function (Builder $query) {
return $query->where('name', 'like', "%{$this->search}%");
})
->paginate($this->quantity)
->withQueryString(),
'type' => 'data',
];
}
}; ?>
 
<div>
<!-- 1: -->
<x-table :$headers :$rows filter paginate id="users">
<!-- The $row represents the instance of \App\Model\User of each row -->
@interact('column_action', $row)
<x-button.circle color="red"
icon="trash"
wire:click="delete('{{ $row->id }}')" />
@endinteract
</x-table>
 
<!-- 2: You can pass extra variables to the directive -->
<x-table :$headers :$rows filter paginate id="users">
@interact('column_action', $row, $type)
<x-button.circle color="red"
icon="trash"
wire:click="delete('{{ $row->id }}', '{{ $type }}')" />
@endinteract
</x-table>
</div>

As mentioned in the Livewire documentation , for cases where you want to render components in a loop, using the Blade @interact directive, you must specify a unique key for each component:

<?php
 
use App\Models\User;
use Livewire\WithPagination;
use Livewire\Volt\Component;
use Illuminate\Database\Eloquent\Builder;
 
new class extends Component {
use WithPagination;
 
public ?int $quantity = 10;
 
public ?string $search = null;
 
public function with(): array
{
return [
'headers' => [
['index' => 'id', 'label' => '#'],
['index' => 'name', 'label' => 'Member'],
['index' => 'action'],
],
'rows' => User::query()
->when($this->search, function (Builder $query) {
return $query->where('name', 'like', "%{$this->search}%");
})
->paginate($this->quantity)
->withQueryString(),
];
}
}; ?>
 
<div>
<x-table :$headers :$rows filter paginate id="users">
@interact('column_action', $row)
<livewire:delete.user :user="$row" :key="uniqid()" />
@endinteract
</x-table>
</div>

You can use $loop inside the @interact directive to interact with the data of each row. The $loop variable is a special variable provided by Blade that contains information about the current iteration of the loop.

An option to expand rows to display sub-content such as nested tables.

<x-table :$headers :$rows expandable>
@interact('sub_table', $row)
<x-table :headers="[
['index' => 'property', 'label' => 'Property'],
['index' => 'value', 'label' => 'Value'],
]" :rows="[
['property' => 'Email', 'value' => $row->email],
['property' => 'Created', 'value' => $row->created_at->format('Y-m-d')],
]" />
@endinteract
</x-table>

An option to compress the padding and display the component compacted.

<x-table :$headers :$rows compact />
Each affected block gained a -compact twin, and the flag swaps the whole string. An application customizing table.td has to customize table.td-compact too if it uses both modes.

Alternatives to use the table out the context of a Livewire component.

Previously the table component was blocked for use outside of Livewire; in v4, we adapted its use for any location, whether inside or outside of Livewire components.
<!-- routes/web.php -> a plain controller, no Livewire anywhere -->
<x-table :$headers
:rows="$users"
:sort="request('sort', ['column' => 'id', 'direction' => 'desc'])"
filter
paginate />
?search=foo&quantity=25&sort[column]=name&sort[direction]=asc&page=2
Feature Inside Livewire Outside
pagination wire:click='gotoPage' <a href>, built from the URLs the paginator already exposes
sorting wire:click='$set' <a href> with the inverted direction
filter wire:model.live AlpineJS rewriting location
loading wire:loading not rendered
selectable entangled array plain array, reported through events
Filtering or sorting drops page ; every other parameter survives. Outside Livewire the paginator is also passed through withQueryString() , or paginating would silently drop the active filter and sort.
# Member Name
1 Alice Morgan
2 Bruno Fernandes
# Member Name
1 Alice Morgan
2 Bruno Fernandes
# Member Name
1 Alice Morgan
2 Bruno Fernandes
<!-- The configured default -->
<x-table :$headers :$rows paginate />
 
<!-- This table only. Accepted: simple, minimal, compact -->
<x-table :$headers :$rows paginate paginator="compact" />
 
<!-- A dotted value is still treated as a view path -->
<x-table :$headers :$rows paginate paginator="components.my-paginator" />
# Member Name
1 Alice Morgan
2 Bruno Fernandes
# Member Name
1 Alice Morgan
2 Bruno Fernandes
# Member Name
1 Alice Morgan
2 Bruno Fernandes
<!-- 3.x -->
<x-table :$headers :$rows paginate simple-pagination />
 
<!-- 4.x: simple-pagination implies paginate -->
<x-table :$headers :$rows simple-pagination />
 
<!-- The paginator variant styles this mode too -->
<x-table :$headers :$rows simple-pagination paginator="minimal" />
<x-table :$headers :$rows simple-pagination paginator="compact" />
# Member Name
1 Alice Morgan
2 Bruno Fernandes
<!-- Anchors on the table itself -->
<x-table :$headers :$rows paginate persistent />
 
<!-- Anchors on an element of your own, so the card header stays in frame -->
<div id="users-table">
<x-card>
<x-table :$headers :$rows paginate persistent="users-table" />
</x-card>
</div>
Value Anchor
false Nothing
persistent The table itself
persistent='users' The given element
An empty string is rejected: it would render href="...#" , which scrolls to the top, and the failure would be silent.
# Member Name
1 Alice Morgan
2 Bruno Fernandes
3 Clara Dubois
4 Daniel Kowalski
5 Elena Rossi
6 Felix Andersen
7 Grace Nakamura
8 Hugo Martinez
9 Isla Petrov
10 Jonas Weber
11 Kira Svensson

Selected:

<div x-data="{ rows: [] }" x-on:selected="rows = $event.detail.rows">
<x-table :$headers :$rows selectable wire:model="selected" />
 
<p>Selected: <b x-text="rows.join(', ') || 'none'"></b></p>
</div>
It comes from a $watch , which also picks up changes pushed from the server into the entangled property.

An option to display a lazy loading skeleton indicator.

# Name E-mail
<!-- Bare flag: 5 rows -->
<x-table :$headers skeleton />
 
<!-- An integer sets the count -->
<x-table :$headers skeleton="8" selectable paginate />

The right place to use is in: placeholder() of a #[Lazy] Livewire component:

#[Lazy]
class UsersTable extends Component
{
public array $headers = [
['index' => 'name', 'label' => 'Name'],
['index' => 'email', 'label' => 'E-mail'],
];
 
public function placeholder(): string
{
return <<<'HTML'
<div>
<x-table :$headers skeleton="5" />
</div>
HTML;
}
}

Code highlighting provided by Torchlight