Building Tables

Preparing your Livewire component

Implement the HasTable interface and use the InteractsWithTable trait:

<?php
 
namespace App\Http\Livewire;
 
use Filament\Tables;
use Illuminate\Contracts\View\View;
use Livewire\Component;
 
class ListPosts extends Component implements Tables\Contracts\HasTable
{
use Tables\Concerns\InteractsWithTable;
 
public function render(): View
{
return view('list-posts');
}
}

In your Livewire component's view, render the table:

<div>
{{ $this->table }}
</div>

Next, add the Eloquent query you would like the table to be based upon in the getTableQuery() method:

<?php
 
namespace App\Http\Livewire;
 
use App\Models\Post;
use Filament\Tables;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Component;
 
class ListPosts extends Component implements Tables\Contracts\HasTable
{
use Tables\Concerns\InteractsWithTable;
 
protected function getTableQuery(): Builder
{
return Post::query();
}
 
public function render(): View
{
return view('list-posts');
}
}

Finally, add any columns, filters, and actions to the Livewire component:

<?php
 
namespace App\Http\Livewire;
 
use App\Models\Post;
use Filament\Tables;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Component;
 
class ListPosts extends Component implements Tables\Contracts\HasTable
{
use Tables\Concerns\InteractsWithTable;
 
protected function getTableQuery(): Builder
{
return Post::query();
}
 
protected function getTableColumns(): array
{
return [ ...
Tables\Columns\ImageColumn::make('author.avatar')
->size(40)
->rounded(),
Tables\Columns\TextColumn::make('title'),
Tables\Columns\TextColumn::make('author.name'),
Tables\Columns\BadgeColumn::make('status')
->colors([
'danger' => 'draft',
'warning' => 'reviewing',
'success' => 'published',
]),
Tables\Columns\BooleanColumn::make('is_featured'),
];
}
 
protected function getTableFilters(): array
{
return [ ...
Tables\Filters\Filter::make('published')
->query(fn (Builder $query): $query => $query->where('is_published', true)),
Tables\Filters\SelectFilter::make('status')
->options([
'draft' => 'Draft',
'in_review' => 'In Review',
'approved' => 'Approved',
]),
];
}
 
protected function getTableActions(): array
{
return [ ...
Tables\Actions\LinkAction::make('edit')
->url(fn (Post $record): string => route('posts.edit', $record)),
];
}
 
protected function getTableBulkActions(): array
{
return [ ...
Tables\Actions\BulkAction::make('delete')
->label('Delete selected')
->color('danger')
->action(function (Collection $records): void {
$records->each->delete();
})
->requiresConfirmation(),
];
}
 
public function render(): View
{
return view('list-posts');
}
}

Visit your Livewire component in the browser, and you should see the table.

Pagination

By default, tables will be paginated. To disable this, you should override the isTablePaginationEnabled() method on your Livewire component:

<?php
 
namespace App\Http\Livewire;
 
use App\Models\Post;
use Filament\Tables;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Component;
 
class ListPosts extends Component implements Tables\Contracts\HasTable
{
use Tables\Concerns\InteractsWithTable;
 
protected function getTableQuery(): Builder
{
return Post::query();
}
 
protected function getTableColumns(): array
{
return [
Tables\Columns\TextColumn::make('title'),
Tables\Columns\TextColumn::make('author.name'),
];
}
 
protected function isTablePaginationEnabled(): bool
{
return false;
}
 
public function render(): View
{
return view('list-posts');
}
}

You may customize the options for the paginated records per page select by overriding the getTableRecordsPerPageSelectOptions() method on your Livewire component:

<?php
 
namespace App\Http\Livewire;
 
use App\Models\Post;
use Filament\Tables;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Component;
 
class ListPosts extends Component implements Tables\Contracts\HasTable
{
use Tables\Concerns\InteractsWithTable;
 
protected function getTableQuery(): Builder
{
return Post::query();
}
 
protected function getTableColumns(): array
{
return [
Tables\Columns\TextColumn::make('title'),
Tables\Columns\TextColumn::make('author.name'),
];
}
 
protected function getTableRecordsPerPageSelectOptions(): array
{
return [10, 25, 50, 100];
}
 
public function render(): View
{
return view('list-posts');
}
}

Empty state

By default, an "empty state" card will be rendered when the table is empty. To customize this, you may define methods on your Livewire component:

<?php
 
namespace App\Http\Livewire;
 
use App\Models\Post;
use Filament\Tables;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Component;
 
class ListPosts extends Component implements Tables\Contracts\HasTable
{
use Tables\Concerns\InteractsWithTable;
 
protected function getTableQuery(): Builder
{
return Post::query();
}
 
protected function getTableColumns(): array
{
return [ ...
Tables\Columns\ImageColumn::make('author.avatar')
->size(40)
->rounded(),
Tables\Columns\TextColumn::make('title'),
Tables\Columns\TextColumn::make('author.name'),
Tables\Columns\BadgeColumn::make('status')
->colors([
'danger' => 'draft',
'warning' => 'reviewing',
'success' => 'published',
]),
Tables\Columns\BooleanColumn::make('is_featured'),
];
}
 
protected function getTableEmptyStateIcon(): ?string
{
return 'heroicon-o-bookmark';
}
 
protected function getTableEmptyStateHeading(): ?string
{
return 'No posts yet';
}
 
protected function getTableEmptyStateDescription(): ?string
{
return 'You may create a post using the button below.';
}
 
protected function getTableEmptyStateActions(): array
{
return [
Tables\Actions\ButtonAction::make('create')
->label('Create post')
->url(route('posts.create'))
->icon('heroicon-o-plus'),
];
}
 
public function render(): View
{
return view('list-posts');
}
}

Using the form builder

Internally, the table builder uses the form builder to implement filtering, actions, and bulk actions. Because of this, the form builder is already set up on your Livewire component and ready to use with your own custom forms.

You may use the default form out of the box:

<?php
 
namespace App\Http\Livewire;
 
use App\Models\Post;
use Filament\Tables;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Component;
 
class ListPosts extends Component implements Tables\Contracts\HasTable
{
use Tables\Concerns\InteractsWithTable;
 
public function mount(): void
{
$this->form->fill();
}
 
protected function getFormSchema(): array
{
return [
// ...
];
}
 
protected function getTableQuery(): Builder ...
{
return Post::query();
}
 
protected function getTableColumns(): array
{
return [
Tables\Columns\ImageColumn::make('author.avatar')
->size(40)
->rounded(),
Tables\Columns\TextColumn::make('title'),
Tables\Columns\TextColumn::make('author.name'),
Tables\Columns\BadgeColumn::make('status')
->colors([
'danger' => 'draft',
'warning' => 'reviewing',
'success' => 'published',
]),
Tables\Columns\BooleanColumn::make('is_featured'),
];
}
 
public function render(): View
{
return view('list-posts');
}
}

You may also register multiple custom forms on your component:

<?php
 
namespace App\Http\Livewire;
 
use App\Models\Post;
use Filament\Tables;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Component;
 
class ListPosts extends Component implements Tables\Contracts\HasTable
{
use Tables\Concerns\InteractsWithTable;
 
public ?Post $postToEdit = null;
 
public function mount(): void
{
$this->createPostForm->fill();
}
 
protected function getCreatePostFormSchema(): array
{
return [
// ...
];
}
 
protected function getEditPostFormSchema(): array
{
return [
// ...
];
}
 
protected function getForms(): array
{
return array_merge($this->getTableForms(), [
'createPostForm' => $this->makeForm()
->schema($this->getCreatePostFormSchema())
->model(Post::class),
'editPostForm' => $this->makeForm()
->schema($this->getEditPostFormSchema())
->model($this->postToEdit),
]);
}
 
protected function getTableQuery(): Builder ...
{
return Post::query();
}
 
protected function getTableColumns(): array
{
return [
Tables\Columns\ImageColumn::make('author.avatar')
->size(40)
->rounded(),
Tables\Columns\TextColumn::make('title'),
Tables\Columns\TextColumn::make('author.name'),
Tables\Columns\BadgeColumn::make('status')
->colors([
'danger' => 'draft',
'warning' => 'reviewing',
'success' => 'published',
]),
Tables\Columns\BooleanColumn::make('is_featured'),
];
}
 
public function render(): View
{
return view('list-posts');
}
}

Still need help? Join our Discord community or open a GitHub discussion

Enjoying Filament?

We are open source at heart. To allow us to build new features, fix bugs, and run the community, we require your financial support.

Sponsor Filament on GitHub