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

  1. Use pagination - Always paginate large datasets
  2. Enable search - Let users find specific records
  3. Add relevant filters - Filter by common attributes
  4. Show loading states - Use wire:loading for feedback
  5. Implement bulk actions - Allow batch operations
  6. Custom column rendering - Format data appropriately
  7. Responsive design - Ensure mobile usability
  8. Sort important columns - Enable sorting on key fields

Next Steps

/