Datatable Components
Feature-rich data table components with sorting, filtering, pagination, bulk actions, and Livewire integration.
Datatable Components
LaraDashboard provides a comprehensive datatable system built on Livewire for displaying, sorting, filtering, and managing tabular data. The datatable includes checkbox selection with Shift+click range select, bulk actions, and responsive design.
Available Datatable Components
| Component | Purpose |
|---|---|
<x-datatable.datatable> |
Main datatable component |
<x-datatable.searchbar> |
Search input for filtering |
<x-datatable.responsive-filters> |
Filter dropdowns |
<x-datatable.skeleton> |
Loading state placeholder |
<x-table.empty-state> |
No results message |
Datatable Component
The main datatable component with full feature support.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
title |
string | '' |
Table title |
enableLivewire |
boolean | true |
Enable Livewire integration |
enableSearchbar |
boolean | true |
Show search input |
searchbarPlaceholder |
string | 'Search...' |
Search placeholder |
customSearchForm |
string | null |
Custom search form HTML |
enableFilters |
boolean | true |
Show filter dropdowns |
filters |
array | [] |
Filter configuration |
customFilters |
string | null |
Custom filter HTML |
enableBulkActions |
boolean | true |
Enable bulk selection |
customBulkActions |
string | null |
Custom bulk actions |
direction |
string | 'desc' |
Default sort direction |
enableNewResourceLink |
boolean | false |
Show create button |
newResourceLinkPermission |
string | '' |
Required permission |
newResourceLinkIcon |
string | 'feather:plus' |
Button icon |
newResourceLinkRouteName |
string | '' |
Route name |
newResourceLinkRouteUrl |
string | '' |
Direct URL |
newResourceLinkLabel |
string | 'Create New' |
Button label |
data |
paginator | required | Paginated data |
enableCheckbox |
boolean | true |
Show row checkboxes |
noResultsMessage |
string | 'No data found.' |
Empty state message |
enablePagination |
boolean | true |
Show pagination |
headers |
array | [] |
Column configuration |
sort |
string | '' |
Current sort column |
perPage |
integer | 10 |
Items per page |
perPageOptions |
array | [10,20,50,100,'All'] |
Page size options |
Header Configuration
Each header in the headers array supports:
| Key | Type | Description |
|---|---|---|
id |
string | Column identifier (model attribute) |
title |
string | Display title |
sortable |
boolean | Enable sorting |
sortBy |
string | Sort column (if different from id) |
width |
string | Column width |
align |
string | Text alignment: left, center, right |
renderContent |
string | Method name for custom rendering |
renderRawContent |
string | Raw HTML content |
Basic Usage with Livewire
// Livewire Component
class UsersList extends Component
{
use WithPagination;
public $search = '';
public $sort = 'created_at';
public $direction = 'desc';
public $perPage = 10;
public $selectedItems = [];
public function render()
{
$users = User::query()
->when($this->search, fn($q) => $q->where('name', 'like', "%{$this->search}%"))
->orderBy($this->sort, $this->direction)
->paginate($this->perPage);
return view('livewire.users-list', [
'users' => $users,
'headers' => $this->getHeaders(),
]);
}
public function getHeaders()
{
return [
['id' => 'name', 'title' => 'Name', 'sortable' => true, 'sortBy' => 'name'],
['id' => 'email', 'title' => 'Email', 'sortable' => true, 'sortBy' => 'email'],
['id' => 'created_at', 'title' => 'Created', 'sortable' => true, 'sortBy' => 'created_at'],
['id' => 'actions', 'title' => 'Actions', 'align' => 'right'],
];
}
public function sortBy($column)
{
if ($this->sort === $column) {
$this->direction = $this->direction === 'asc' ? 'desc' : 'asc';
} else {
$this->sort = $column;
$this->direction = 'asc';
}
}
public function getBulkDeleteAction()
{
return [
'url' => route('admin.users.bulk-delete'),
'method' => 'DELETE',
];
}
// Custom column rendering
public function renderNameColumn($item, $header)
{
return view('components.users.name-cell', ['user' => $item])->render();
}
public function renderActionsColumn($item, $header)
{
return view('components.users.actions-cell', ['user' => $item])->render();
}
}
{{-- Blade Template --}}
<x-datatable.datatable
:data="$users"
:headers="$headers"
:sort="$sort"
:direction="$direction"
:perPage="$perPage"
searchbarPlaceholder="{{ __('Search users...') }}"
:enableNewResourceLink="true"
newResourceLinkPermission="create users"
newResourceLinkRouteName="admin.users.create"
newResourceLinkLabel="{{ __('Add User') }}"
/>
With Filters
// In Livewire component
public $statusFilter = '';
public $roleFilter = '';
public function getFilters()
{
return [
[
'id' => 'status',
'label' => 'Status',
'options' => [
'' => 'All Statuses',
'active' => 'Active',
'inactive' => 'Inactive',
],
],
[
'id' => 'role',
'label' => 'Role',
'options' => Role::pluck('name', 'id')->prepend('All Roles', '')->toArray(),
],
];
}
public function hasActiveFilters()
{
return !empty($this->statusFilter) || !empty($this->roleFilter);
}
<x-datatable.datatable
:data="$users"
:headers="$headers"
:filters="$this->getFilters()"
...
/>
Custom Column Rendering
There are multiple ways to customize column content:
Method 1: Auto-discovered Method
Name your method render{PascalCaseId}Column:
// For header with id='status', create:
public function renderStatusColumn($item, $header)
{
$statusClass = $item->is_active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800';
return "<span class='px-2 py-1 rounded text-xs {$statusClass}'>"
. ($item->is_active ? 'Active' : 'Inactive')
. "</span>";
}
// For header with id='created_at', create:
public function renderCreatedAtColumn($item, $header)
{
return $item->created_at->format('M d, Y');
}
Method 2: Explicit Method Reference
$headers = [
[
'id' => 'avatar',
'title' => 'Avatar',
'renderContent' => 'renderAvatarCell',
],
];
public function renderAvatarCell($item, $header)
{
return '<img src="' . $item->avatar_url . '" class="w-8 h-8 rounded-full" />';
}
Method 3: Raw Content
$headers = [
[
'id' => 'badge',
'title' => '',
'renderRawContent' => '<span class="w-2 h-2 rounded-full bg-primary"></span>',
],
];
Bulk Actions
The datatable includes built-in bulk delete functionality. For custom bulk actions:
<x-datatable.datatable
:data="$users"
:headers="$headers"
:customBulkActions="view('partials.custom-bulk-actions')->render()"
/>
{{-- partials/custom-bulk-actions.blade.php --}}
<div x-show="selectedItems.length > 0" class="flex items-center gap-2">
<x-buttons.button
@click="$wire.bulkActivate(selectedItems)"
variant="success"
icon="lucide:check"
>
Activate Selected
</x-buttons.button>
<x-buttons.button
@click="$wire.bulkDeactivate(selectedItems)"
variant="warning"
icon="lucide:x"
>
Deactivate Selected
</x-buttons.button>
</div>
Column Visibility (v1.4.5+)
Every datatable shows a Columns dropdown so users can hide and show columns. The choice is remembered in the browser (local storage), per table class — including generated module tables.
To turn it off, or to keep two tables' preferences apart, override these methods in your datatable class:
public function usesColumnVisibility(): bool
{
return false; // hide the Columns dropdown
}
public function getColumnVisibilityStorageKey(): string
{
// Default: 'datatable-visible-columns-' . Str::kebab(class_basename(static::class))
return 'datatable-visible-columns-orders-archive';
}
Page Scroll and Sticky Headers
List pages use a unified page scroll by default: the page title row, the column headers and the pagination bar stay pinned while the whole page scrolls, so long tables never trap the user in an inner scroll area. Wide tables still scroll horizontally, and on small screens the toolbar, pager and filter dropdowns stack so they stay usable.
To use an inner table scroller instead:
public function usesUnifiedPageScroll(): bool
{
return false;
}
Range Selection
Hold Shift and click a second checkbox to select every row between it and the last one you clicked (current page). Bulk actions then apply to the whole range.
Real Example from Codebase
{{-- Users datatable --}}
<x-datatable.datatable
:data="$users"
:headers="[
['id' => 'name', 'title' => __('Name'), 'sortable' => true, 'sortBy' => 'name'],
['id' => 'email', 'title' => __('Email'), 'sortable' => true, 'sortBy' => 'email'],
['id' => 'roles', 'title' => __('Roles')],
['id' => 'status', 'title' => __('Status'), 'sortable' => true, 'sortBy' => 'is_active'],
['id' => 'created_at', 'title' => __('Created'), 'sortable' => true, 'sortBy' => 'created_at'],
['id' => 'actions', 'title' => __('Actions'), 'align' => 'right'],
]"
:sort="$sort"
:direction="$direction"
:perPage="$perPage"
:filters="$this->getFilters()"
searchbarPlaceholder="{{ __('Search users by name or email...') }}"
:enableNewResourceLink="true"
newResourceLinkPermission="create users"
newResourceLinkRouteName="admin.users.create"
newResourceLinkLabel="{{ __('Add User') }}"
newResourceLinkIcon="feather:user-plus"
/>
Searchbar Component
Search input component for the datatable.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder |
string | 'Search...' |
Input placeholder |
enableLivewire |
boolean | true |
Enable wire:model |
Basic Usage
<x-datatable.searchbar
placeholder="{{ __('Search...') }}"
/>
Standalone Usage
<x-datatable.searchbar
placeholder="Search products..."
:enableLivewire="true"
/>
Responsive Filters Component
Filter dropdown controls for the datatable.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
filters |
array | [] |
Filter configuration |
enableLivewire |
boolean | true |
Livewire integration |
hasActiveFilters |
boolean | false |
Show active indicator |
Filter Configuration
$filters = [
[
'id' => 'status', // Maps to $status property
'label' => 'Status',
'options' => [
'' => 'All',
'active' => 'Active',
'pending' => 'Pending',
'inactive' => 'Inactive',
],
],
[
'id' => 'category_id',
'label' => 'Category',
'options' => Category::pluck('name', 'id')->prepend('All Categories', '')->toArray(),
],
];
Usage
<x-datatable.responsive-filters
:filters="$filters"
:hasActiveFilters="$this->hasActiveFilters()"
/>
Skeleton Component
Loading placeholder while datatable data loads.
Basic Usage
{{-- Shows while loading --}}
<div wire:loading wire:target="search">
<x-datatable.skeleton />
</div>
Empty State Component
Displayed when no results are found.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
message |
string | 'No results found' |
Display message |
icon |
string | 'lucide:inbox' |
Icon to display |
Basic Usage
<x-table.empty-state
message="{{ __('No users found matching your criteria.') }}"
icon="lucide:users"
/>
Complete Livewire Component Example
<?php
namespace App\Livewire\Admin;
use App\Models\User;
use App\Models\Role;
use Livewire\Component;
use Livewire\WithPagination;
class UsersDatatable extends Component
{
use WithPagination;
public $search = '';
public $sort = 'created_at';
public $direction = 'desc';
public $perPage = 10;
public $selectedItems = [];
public $statusFilter = '';
public $roleFilter = '';
protected $queryString = [
'search' => ['except' => ''],
'sort' => ['except' => 'created_at'],
'direction' => ['except' => 'desc'],
'perPage' => ['except' => 10],
];
public function updatingSearch()
{
$this->resetPage();
}
public function sortBy($column)
{
if ($this->sort === $column) {
$this->direction = $this->direction === 'asc' ? 'desc' : 'asc';
} else {
$this->sort = $column;
$this->direction = 'asc';
}
}
public function getHeaders()
{
return [
['id' => 'name', 'title' => 'Name', 'sortable' => true, 'sortBy' => 'name'],
['id' => 'email', 'title' => 'Email', 'sortable' => true, 'sortBy' => 'email'],
['id' => 'roles', 'title' => 'Roles'],
['id' => 'status', 'title' => 'Status', 'sortable' => true, 'sortBy' => 'is_active'],
['id' => 'created_at', 'title' => 'Created', 'sortable' => true],
['id' => 'actions', 'title' => 'Actions', 'align' => 'right'],
];
}
public function getFilters()
{
return [
[
'id' => 'statusFilter',
'label' => 'Status',
'options' => [
'' => 'All',
'active' => 'Active',
'inactive' => 'Inactive',
],
],
[
'id' => 'roleFilter',
'label' => 'Role',
'options' => Role::pluck('name', 'id')
->prepend('All Roles', '')
->toArray(),
],
];
}
public function hasActiveFilters()
{
return !empty($this->statusFilter) || !empty($this->roleFilter);
}
public function getBulkDeleteAction()
{
return [
'url' => null, // Use Livewire method
'method' => 'DELETE',
];
}
public function bulkDelete()
{
User::whereIn('id', $this->selectedItems)->delete();
$this->selectedItems = [];
$this->dispatch('resetSelectedItems');
$this->dispatch('notify', [
'variant' => 'success',
'title' => 'Deleted',
'message' => 'Selected users have been deleted.',
]);
}
// Custom column renderers
public function renderNameColumn($user, $header)
{
return '<div class="flex items-center gap-3">
<img src="' . $user->avatar_url . '" class="w-8 h-8 rounded-full" />
<span>' . e($user->name) . '</span>
</div>';
}
public function renderRolesColumn($user, $header)
{
$badges = $user->roles->map(function ($role) {
return '<span class="px-2 py-1 text-xs rounded bg-primary/10 text-primary">'
. e($role->name) . '</span>';
})->join(' ');
return '<div class="flex flex-wrap gap-1">' . $badges . '</div>';
}
public function renderStatusColumn($user, $header)
{
$class = $user->is_active
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
$label = $user->is_active ? 'Active' : 'Inactive';
return "<span class='px-2 py-1 text-xs rounded {$class}'>{$label}</span>";
}
public function renderCreatedAtColumn($user, $header)
{
return $user->created_at->format('M d, Y');
}
public function renderActionsColumn($user, $header)
{
return view('livewire.users.action-buttons', ['user' => $user])->render();
}
public function render()
{
$users = User::query()
->with('roles')
->when($this->search, function ($query) {
$query->where(function ($q) {
$q->where('name', 'like', "%{$this->search}%")
->orWhere('email', 'like', "%{$this->search}%");
});
})
->when($this->statusFilter, function ($query) {
$query->where('is_active', $this->statusFilter === 'active');
})
->when($this->roleFilter, function ($query) {
$query->whereHas('roles', fn($q) => $q->where('id', $this->roleFilter));
})
->orderBy($this->sort, $this->direction)
->paginate($this->perPage);
return view('livewire.admin.users-datatable', [
'users' => $users,
'headers' => $this->getHeaders(),
'filters' => $this->getFilters(),
]);
}
}
Best Practices
- Use pagination - Always paginate large datasets
- Enable search - Let users find specific records
- Add relevant filters - Filter by common attributes
- Show loading states - Use
wire:loadingfor feedback - Implement bulk actions - Allow batch operations
- Custom column rendering - Format data appropriately
- Responsive design - Ensure mobile usability
- Sort important columns - Enable sorting on key fields