Ask AI about this page

Table Component

Introduction

The table component provides a powerful, composable system for building feature-rich data tables. Built with Livewire and Alpine.js, it offers pagination, sorting, searching, selection, bulk actions, column visibility controls, and more all with excellent accessibility and a clean, modern design.

Our approach uses composable traits on the backend and Blade components on the frontend, giving you complete control over your table's structure and behavior while maintaining clean, reusable code.

Installation

Use the sheaf artisan command to install the table component:

php artisan sheaf:install data-table

Basic Static Table

Let's start with a simple static table without any dynamic features.

Name
Email
Role
Alice Johnson alice@example.com Admin
Bob Smith bob@example.com Editor
Carol White carol@example.com Viewer
<x-ui.table>
    <x-ui.table.header>
        <x-ui.table.columns>
            <x-ui.table.head>Name</x-ui.table.head>
            <x-ui.table.head>Email</x-ui.table.head>
            <x-ui.table.head>Role</x-ui.table.head>
        </x-ui.table.columns>
    </x-ui.table.header>

    <x-ui.table.rows>
        <x-ui.table.row>
            <x-ui.table.cell>Alice Johnson</x-ui.table.cell>
            <x-ui.table.cell>alice@example.com</x-ui.table.cell>
            <x-ui.table.cell>Admin</x-ui.table.cell>
        </x-ui.table.row>
        <!-- More rows... -->
    </x-ui.table.rows>
</x-ui.table>

Sorting

Usage

Add column sorting with visual indicators and flexible behavior. To enable sorting, first add the App\Livewire\Concerns\WithSorting trait to your Livewire component:

{~
use App\Models\User;
use App\Livewire\Concerns\WithSorting;
use App\Livewire\Concerns\WithPagination;

class UsersTable extends Component
{~}
    
    use WithSorting;

    public function render()
    {
        $users = User::query()
            ->when(filled($this->sortBy), function ($query) {
                return $this->applySorting($query);
            })
            ->paginate();

        return view('livewire.users-table', [
            'users' => $users,
        ]);
    }
}

The $this->sortBy and $this->applySorting() methods come from the WithSorting trait.

In your table header view, mark sortable columns and pass the current sort state:

<x-ui.table.head
    column="name"
    sortable
    :currentSortBy="$sortBy"
    :currentSortDir="$sortDir"
>
    Name
</x-ui.table.head>

Key attributes:

  • column — Database column name
  • sortable — Marks the column as sortable
  • currentSortBy and currentSortDir — Reactive Livewire properties tracking sort state

Note: If you want to restrict sorting on the backend, you can use the sortableColumns() method on the component class.

Sorting Variants

There are two sorting variants:

  • default — Shows sorting icons on hover and cycles sorting on click
  • dropdown — Opens a menu where the user explicitly chooses the sorting direction
<x-ui.table.head
{~    column="name"
    sortable
    :currentSortBy="$sortBy"
    :currentSortDir="$sortDir"~}
    variant="default" <!-- default -->
    <!-- or -->
    variant="dropdown"
>
    Name
</x-ui.table.head>

You can see these variants in action on our interactive Math Theorems demo. Sorting by year uses the dropdown variant, while mathematician and difficulty columns use the default variant.

Sorting Algorithm

By default, sorting uses Eloquent’s native orderBy method.

For advanced cases, you may override sorting per column by defining a custom sorting algorithm in your Livewire component using sortUsingAlgorithm.

If a custom algorithm handles the active column, it is applied. If not, the system falls back automatically to the default SQL sorting.

public function sortUsingAlgorithm($query, string $column, string $direction): ?Builder
{
    if ($column === 'status') {
        return $query->orderByRaw("
            CASE
                WHEN status = 'in_dev' THEN 1
                WHEN status = 'ready' THEN 2
                WHEN status = 'production' THEN 3
            END {$direction}
        ");
    }

    return null;
}

the contract

  • Return a Builder when the column is handled.
  • Return null to delegate to the default sorter.

Pagination

Add pagination to your table by passing a Laravel paginator to the table component.

First, create a Livewire component that uses the App\Livewire\Concerns\WithPagination trait:

use App\Livewire\Concerns\WithPagination;

class UsersTable extends Component
{
    use WithPagination;

    public function render()
    {
        $users = User::query()
            ->paginate();
            // or (if using length-aware paginator with full variant)
            ->paginate($this->perPage);

        return view('livewire.users-table', [
            'users' => $users,
        ]);
    }
}

Then pass the paginator to the paginator prop in the view:

<x-ui.table 
    :paginator="$users"
>
    <!-- table contents... -->
</x-ui.table>

More About Pagination

The pagination component has very detailed documentation and demos there Visit Docs

Search

Enable real-time search across your table data. To enable search, first add the App\Livewire\Concerns\WithSearch trait to your Livewire component:

{~
use App\Models\User;
use App\Livewire\Concerns\WithSearch;
use App\Livewire\Concerns\WithPagination;

class UsersTable extends Component
{~}
    
    use WithSearch;

    public function render()
    {
        $users = User::query()
            ->when(filled($this->searchQuery), function ($query) {
                return $this->applySearch($query);
            })
            ->paginate();

        return view('livewire.users-table', [
            'users' => $users,
        ]);
    }

    protected function applySearch($query)
    {
        return $query->where('name', 'like', '%'.$this->searchQuery.'%');
    }
}

The applySearch() method is where you define your search logic.

Then bind an input to the search query:

<x-ui.input 
    placeholder="Search..." 
    leftIcon="magnifying-glass" 
    wire:model.live="searchQuery" <!-- this is what's important -->
/>

See the search implementation in the complete guide below for a real-world layout.


Selection

The component comes with all the logic you need to add row selection and "select all" functionality for handling bulk actions. To enable selection, first add the App\Livewire\Concerns\WithSelection trait to your Livewire component:

{~
use App\Models\User;
use App\Livewire\Concerns\WithSelection;

class UsersTable extends Component
{~}
    
    use WithSelection;

    public function render()
    {
        $users = User::query()->paginate();

        // This is crucial - it tells us about the visible rows on the page
        $this->visibleIds = $users->pluck('id')
            ->map(fn ($id) => (string) $id)
            ->toArray();

        return view('livewire.users-table', [
            'users' => $users,
        ]);
    }
}

Then in the view, add :checkboxId to each table.row component to show the checkbox at the start of the row. To add "check all" functionality, add withCheckAll to the table.columns component:

<x-ui.table.columns
    withCheckAll
>
    <!-- other head components -->
</x-ui.table.columns>

<!-- AND -->

<x-ui.table.row 
    :checkboxId="$user->id"
    :key="$user->id"
>
    <!-- cells... -->
</x-ui.table.row>

Now you can perform operations on the selected IDs like this:

public function deleteSelected()
{
    User::query()->whereIn('id', $this->selectedIds)->delete();
}

public function archiveSelected()
{
    User::query()->whereIn('id', $this->selectedIds)->each->archive();
}

Loading States

Due to the nature of datatables being usually data-heavy, this component comes with a clean way to handle loading states.

To enable loading indicators, just add wire:loading to the table component:

<x-ui.table 
    wire:loading
>
    <!-- table content... -->
</x-ui.table>

For convenience, we've made it easy to enable loading on sorting, searching, and pagination by passing them comma-separated to the loadOn prop:

<x-ui.table 
    wire:loading
    loadOn="pagination, search, sorting"
>
    <!-- table content... -->
</x-ui.table>

To add other targets, you can use wire:target as you would in regular usage:

<x-ui.table 
    wire:loading
    wire:target="customAction, archiveSomething"
>
    <!-- table content... -->
</x-ui.table>

you can completly customize the loading state stuffs by using the loading slot

<x-ui.table>
    <x-slot:loading class="size-12 bg-white/5 rounded-lg">
            <x-ui.icon.loading class="dark:invert size-10"/>
    </x-slot:loading>
</x-ui.table>


Ordering

Ordering allows users to rearrange table rows visually and persist the new order on the backend.

Usage

To enable row reordering, mark the table as reorderable.

<x-ui.table 
    reorderable
>
    <!-- table contents -->
</x-ui.table>

Each table row must expose its current order value. This value determines the row's position.

<x-ui.table.rows>
    @forelse($users as $user)
        <x-ui.table.row 
            :checkboxId="$user->id" 
            :order="$user->order" 
            :key="$user->id"
        >
            <!-- ... -->
        </x-ui.table.row>
    @endforelse
</x-ui.table.rows>

When a row is moved, the table emits the moved item identifier and its target position.

Backend Handling

Add the App\Livewire\Concerns\Reorderable trait to your component and implement handleReordering:

use App\Livewire\Concerns\Reorderable;

class UsersTable extends Component
{
    use Reorderable;

    public function render()
    {
        $users = User::query()->paginate();

        return view('livewire.users-table', ['users' => $users]);
    }

    public function handleReordering($item, int $position): void
    {
        $this->reorderTransaction(function () use ($item, $position) {
            $user = User::findOrFail($item);

            $this->reorderWithinScope(model: $user, newPosition: $position);
        });
    }
}

If you prefer a different method name, update the x-sort="$wire.handleReordering" binding on table.rows.

Scoping Reorders

If your rows belong to a parent, a category, a board column, a project, pass a scope closure so only sibling rows are shifted. Without it, the trait shifts every row in the table.

public function handleReordering($item, int $position): void
{
    $this->reorderTransaction(function () use ($item, $position) {
        $task = Task::findOrFail($item);

        $this->reorderWithinScope(
            model: $task,
            newPosition: $position,
            scope: fn ($q) => $q->where('project_id', $task->project_id)
        );
    });
}

Stickiness

Make columns or headers stick to the viewport while scrolling.

Sticky Header

Keep the header visible while scrolling through long tables:

Product
Price
Stock
Product 1 $38 8
Product 2 $34 29
Product 3 $69 46
Product 4 $73 46
Product 5 $91 36
Product 6 $48 9
Product 7 $50 11
Product 8 $46 8
Product 9 $92 19
Product 10 $100 2
Product 11 $78 3
Product 12 $57 28
Product 13 $65 13
Product 14 $64 19
Product 15 $23 22
Product 16 $45 25
Product 17 $14 12
Product 18 $32 13
Product 19 $54 46
Product 20 $80 38
<x-ui.table class="max-h-96">
    <x-ui.table.header sticky class="dark:bg-neutral-900 bg-white">
        <x-ui.table.columns>
            <x-ui.table.head>Product</x-ui.table.head>
            <x-ui.table.head>Price</x-ui.table.head>
        </x-ui.table.columns>
    </x-ui.table.header>
    
    <!-- ... -->
</x-ui.table>

Sticky Column

Make the first column stick when scrolling horizontally:

Name
Email
Department
Location
Start Date
End Date
Alice Johnson alice@company.com Engineering San Francisco, CA 2023-01-15 2029-01-15
Bob Smith bob@company.com Marketing New York, NY 2022-08-20 2028-08-20
<x-ui.table.header sticky class="dark:bg-neutral-900 bg-white">
    <x-ui.table.columns>
        <x-ui.table.head 
            sticky 
            class="dark:bg-neutral-900 bg-white"
        >
            Name
        </x-ui.table.head>
        <!-- Other headers... -->
    </x-ui.table.columns>
</x-ui.table.header>

<x-ui.table.rows>
    <x-ui.table.row>
        <x-ui.table.cell 
            sticky 
            class="dark:bg-neutral-950 bg-neutral-50"
        >
            Alice Johnson
        </x-ui.table.cell>
        <!-- Other cells... -->
    </x-ui.table.row>
</x-ui.table.rows>

Note: When using sticky headers or columns, always apply a background color to prevent content overlap during scrolling.

when you've enable reorderable feature with sticky columns, adding background color to rows will kill the animated opacity for the sticky cell, so it's better to add the background only if there is not a sorting happenings and you can do that easily by using [body:not(.sorting)_&]: as a variant there.

<x-ui.table.row 
    :checkboxId="$theorem->id" 
    :key="$theorem->id"
>
    <x-ui.table.cell 
        sticky 
        class="[body:not(.sorting)_&]:dark:bg-neutral-950 [body:not(.sorting)_&]:bg-neutral-50"
    >
        {{ $theorem->id }}
    </x-ui.table.cell>
    <!-- .... -->
</x-ui.table.row>

Implementation Guide

Overview

This guide walks you through building a polished, feature-rich data table using the table component alongside other utilities. We'll display a list of mathematical theorems, including their discovery year and the mathematicians behind them.

in this guide we're going to build a powerful datatable demo Visit the Demo

theorems table
Theorems Data Table Demo

Just Read and understand I've assemble all the code at the end for the demo Go Below to Full Code

Setup Theorems Data

First, create a Theorem model with fields: id, name, mathematician, field, year_discovered, difficulty_level, is_proven, statement, and applications.

For this demo, we use the Sushi package with an in-memory array model. Our App\Models\Theorem looks like this:

<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Sushi\Sushi;

class Theorem extends Model
{
    use Sushi;

    protected $casts = [
        'year_discovered' => 'integer',
        'difficulty_level' => 'integer',
        'is_proven' => 'boolean',
    ];

    protected function getRows(): array
    {
        return [
            ['id' => 1, 'name' => 'Pythagorean Theorem', 'mathematician' => 'Pythagoras', 'field' => 'Geometry', 'year_discovered' => -500, 'difficulty_level' => 2, 'is_proven' => true, 'statement' => 'In a right triangle, a² + b² = c²', 'applications' => 'Architecture, Navigation'],
            ['id' => 2, 'name' => 'Fundamental Theorem of Calculus', 'mathematician' => 'Isaac Newton & Gottfried Leibniz', 'field' => 'Analysis', 'year_discovered' => 1666, 'difficulty_level' => 7, 'is_proven' => true, 'statement' => 'Links differentiation and integration', 'applications' => 'Physics, Engineering'],
            // ... (rest of theorems)
        ];
    }
}

Note: This demo uses array-based models for simplicity. Your real application will use standard Laravel Eloquent models, but the integration remains the same regardless of data source.

Table with Pagination

Add the App\Livewire\Concerns\WithPagination trait to your Livewire component to enable pagination:

use App\Models\Theorem;
use App\Livewire\Concerns\WithPagination;
use Illuminate\Database\Eloquent\Builder;

class Theorems extends Component
{
    use WithPagination;

    public function render()
    {
        $theorems = $this->baseQuery()->paginate($this->perPage);

        return view('livewire.theorems', [
            'theorems' => $theorems,
        ]);
    }

    protected function baseQuery(): Builder
    {
        return Theorem::query();
    }
}

Key points:

  • We use a custom WithPagination trait that extends Livewire's native pagination with a $perPage property and reactive updates
  • The baseQuery() method encapsulates the core query builder, making it reusable for sorting, filtering, and other operations in later steps

The view integrates pagination and displays theorem details with badges and icons:

<x-ui.table 
    :paginator="$theorems"
    pagination:variant="full"
>
    <x-ui.table.header>
        <x-ui.table.columns>
            <x-ui.table.head>ID</x-ui.table.head>
            <x-ui.table.head>Theorem</x-ui.table.head>
            <x-ui.table.head>Mathematician</x-ui.table.head>
            <x-ui.table.head>Field</x-ui.table.head>
            <x-ui.table.head>Year</x-ui.table.head>
            <x-ui.table.head>Difficulty</x-ui.table.head>
            <x-ui.table.head>Status</x-ui.table.head>
        </x-ui.table.columns>
    </x-ui.table.header>

    <x-ui.table.rows>
        @forelse($theorems as $theorem)
            <x-ui.table.row :key="$theorem->id">
                <x-ui.table.cell sticky class="dark:bg-neutral-950 bg-neutral-50">
                    {{ $theorem->id }}
                </x-ui.table.cell>
                
                <x-ui.table.cell>
                    <div class="max-w-xs">
                        <div class="font-medium text-neutral-900 dark:text-neutral-100">
                            {{ $theorem->name }}
                        </div>
                        <div class="text-xs text-neutral-500 dark:text-neutral-400 mt-1 line-clamp-2">
                            {{ $theorem->statement }}
                        </div>
                    </div>
                </x-ui.table.cell>
                
                <x-ui.table.cell>
                    <div class="text-sm text-neutral-700 dark:text-neutral-300">
                        {{ $theorem->mathematician }}
                    </div>
                </x-ui.table.cell>
                
                <x-ui.table.cell>
                    @php
                        $fieldColors = [
                            'Number Theory' => 'purple',
                            'Analysis' => 'blue',
                            'Geometry' => 'green',
                            // ... more field colors
                        ];
                        $color = $fieldColors[$theorem->field] ?? 'neutral';
                    @endphp
                    <x-ui.badge :color="$color" size="sm" variant="outline">
                        {{ $theorem->field }}
                    </x-ui.badge>
                </x-ui.table.cell>
                
                <x-ui.table.cell>
                    <div class="text-sm font-mono text-neutral-600 dark:text-neutral-400">
                        {{ $theorem->year_discovered < 0 ? abs($theorem->year_discovered) . ' BC' : $theorem->year_discovered }}
                    </div>
                </x-ui.table.cell>
                
                <x-ui.table.cell>
                    <div class="flex items-center gap-1">
                        @for($i = 1; $i <= min($theorem->difficulty_level, 10); $i++)
                            <svg class="size-3 {{ $i <= 3 ? 'text-green-500' : ($i <= 6 ? 'text-yellow-500' : 'text-red-500') }}" fill="currentColor" viewBox="0 0 20 20">
                                <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
                            </svg>
                        @endfor
                    </div>
                    <div class="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
                        Level {{ $theorem->difficulty_level }}
                    </div>
                </x-ui.table.cell>
                
                <x-ui.table.cell>
                    @if($theorem->is_proven)
                        <x-ui.badge icon="check-circle" color="green" size="sm" variant="outline">
                            Proven
                        </x-ui.badge>
                    @else
                        <x-ui.badge icon="exclamation-triangle" color="orange" size="sm" variant="outline">
                            Conjecture
                        </x-ui.badge>
                    @endif
                </x-ui.table.cell>
            </x-ui.table.row>
        @empty
            <!-- to implement in upcoming section -->
        @endforelse
    </x-ui.table.rows>
</x-ui.table>

Add Sorting

Enable column sorting with visual indicators and flexible behavior.

Step 1: Add the Trait

Include the App\Livewire\Concerns\WithSorting trait in your Livewire component:

use App\Models\Theorem;
use App\Livewire\Concerns\WithSorting;
use App\Livewire\Concerns\WithPagination;

class Theorems extends Component
{
    use WithSorting;
    use WithPagination;

    public function render()
    {
        $theorems = $this->baseQuery()
            ->when(filled($this->sortBy), function ($query) {
                return $this->applySorting($query);
            })
            ->paginate($this->perPage);

        return view('livewire.theorems', [
            'theorems' => $theorems,
        ]);
    }

    protected function baseQuery(): Builder
    {
        return Theorem::query();
    }
}

Step 2: Update the View

Let's make mathematician, year, and difficulty sortable, while making sort by year special by using the dropdown sorting variant:

<x-ui.table 
    :paginator="$theorems"
    pagination:variant="full"
    loadOn="pagination, sorting"
>
    <x-ui.table.header>
        <x-ui.table.columns>
            <!-- other headers -->
            <x-ui.table.head
                column="mathematician"
                sortable
                :currentSortBy="$sortBy"
                :currentSortDir="$sortDir"
            >
                Mathematician
            </x-ui.table.head>
            
            <x-ui.table.head>
                Field
            </x-ui.table.head>
            
            <x-ui.table.head
                column="year_discovered"
                sortable
                variant="dropdown"
                :currentSortBy="$sortBy"
                :currentSortDir="$sortDir"
            >
                Year
            </x-ui.table.head>
            
            <x-ui.table.head
                column="difficulty_level"
                sortable
                :currentSortBy="$sortBy"
                :currentSortDir="$sortDir"
            >
                Difficulty
            </x-ui.table.head>
            
            <x-ui.table.head>
                Status
            </x-ui.table.head>
        </x-ui.table.columns>
    </x-ui.table.header>
    
    <!-- table rows... -->
</x-ui.table>

Add Search

Enable real-time search across your table data.

Step 1: Add the Trait

Include the WithSearch trait:

<?php

namespace App\Livewire;

use App\Models\Theorem;
use Livewire\Component;
use App\Livewire\Concerns\WithPagination;
use App\Livewire\Concerns\WithSorting;
use App\Livewire\Concerns\WithSearch;

class Theorems extends Component
{
    use WithPagination;
    use WithSorting;
    use WithSearch;

    public function render()
    {
        $theorems = $this->baseQuery()
            ->when(filled($this->sortBy), function ($query) {
                return $this->applySorting($query);
            })
            ->when(filled($this->searchQuery), function ($query) {
                return $this->applySearch($query);
            })
            ->paginate($this->perPage);

        return view('livewire.theorems', [
            'theorems' => $theorems,
        ]);
    }

    protected function applySearch($query)
    {
        return $query->where('name', 'like', '%'.$this->searchQuery.'%')
            ->orWhere('mathematician', 'like', '%'.$this->searchQuery.'%')
            ->orWhere('field', 'like', '%'.$this->searchQuery.'%')
            ->orWhere('statement', 'like', '%'.$this->searchQuery.'%');
    }

    protected function baseQuery(): Builder
    {
        return Theorem::query();
    }
}

Step 2: Add the Search Input

First, let's wrap our table and all filter logic into the table.container Blade component, which is responsible for managing padding between pagination, table, and filters in a clean way:

<x-ui.table.container>
    <div class="flex items-center">
        {{-- SEARCH INPUT --}}
        <div class="ml-auto">
            <x-ui.input 
                class="[&_input]:bg-transparent" <!-- target underlying input and make it transparent -->
                placeholder="Search..." 
                leftIcon="magnifying-glass" 
                wire:model.live="searchQuery"
            />
        </div>
    </div>

    <x-ui.table 
        :paginator="$theorems"
        pagination:variant="full"
        loadOn="pagination, search, sorting"
    >
        <!-- table contents... -->
    </x-ui.table>
</x-ui.table.container>

Handle Empty States

Provide helpful feedback when no results are found using our empty state component:

No theorems found

Try adjusting your search or filters.

<x-ui.table.rows>
    @forelse ($theorems as $user)
       <!-- hidden contents -->
    @empty
      <x-ui.table.empty>
            <x-ui.empty>
                <x-ui.empty.media>
                    <x-ui.icon name="inbox" class="size-10" />
                </x-ui.empty.media>
                <x-ui.empty.contents>
                    <h3 class="text-lg font-semibold">No theorems found</h3>
                    <p class="text-sm text-neutral-500">
                        Try adjusting your search or create a new user.
                    </p>
                </x-ui.empty.contents>
            </x-ui.empty>
        </x-ui.table.empty>
    @endforelse
</x-ui.table.rows>

Add Checkbox Selection

Enable row selection with checkboxes and a "select all" header.

Step 1: Add the Trait

Include the WithSelection trait:

<?php

namespace App\Livewire;

use App\Models\Theorem;
use Livewire\Component;
use App\Livewire\Concerns\WithPagination;
use App\Livewire\Concerns\WithSorting;
use App\Livewire\Concerns\WithSearch;
use App\Livewire\Concerns\WithSelection;

class Theorems extends Component
{
{~    use WithPagination;
    use WithSorting;
    use WithSearch;~}
    use WithSelection;

    public function render()
    {
        $theorems = $this->baseQuery()->...();

        // Store visible IDs for "select all" functionality
        $this->visibleIds = $theorems->pluck('id')
            ->map(fn ($id) => (string) $id)
            ->toArray();

        return view('livewire.theorems', [
            'theorems' => $theorems,
        ]);
    }
}

Step 2: Add Checkboxes to the View

Enable the "check all" header and add checkboxes to rows:

<x-ui.table.columns 
    withCheckAll
>
    <!-- header cells... -->
</x-ui.table.columns>

<!-- AND -->

<x-ui.table.rows>
    @foreach ($theorems as $theorem)
        <x-ui.table.row 
            :key="$theorem->id"
            :checkboxId="$theorem->id"
        >
            <!-- cells... -->
        </x-ui.table.row>
    @endforeach
</x-ui.table.rows>

Bulk Actions (Delete and CSV Export Example)

Let's add bulk action buttons that only show when there are selected rows. We'll implement delete and CSV export for the selected rows.

Step 1: Add the CSV Export Trait

Include the CanExportCsv trait:

<?php

namespace App\Livewire;

use App\Livewire\Concerns\CanExportCsv;
use Livewire\Attributes\Renderless;

class Theorems extends Component
{
{~    use WithPagination;
    use WithSorting;
    use WithSearch;
    use WithSelection;~}
    use CanExportCsv;

    #[Renderless]
    public function exportSelected()
    {
        $theorems = $this->baseQuery();
        
        if (filled($this->selectedIds)) {
            $theorems = $theorems->whereIn('id', $this->selectedIds);
        }
        
        // Apply filters like search and sorting if you want
        // then convert them into CSV...
        return $this->csv($theorems->get());
    }

    public function deleteSelected()
    {
        // ⚠️ Don't forget validation & authorization

        // Gate::authorize('delete-theorem', Theorem::class);
        
        $this->baseQuery()
            ->whereIn('id', $this->selectedIds)
            ->delete();

        // You may clear selection after deletes
        $this->deselectAll();
    }

    // other methods...
}

Step 2: Add Bulk Actions UI

Add bulk action controls that appear when rows are selected:

<x-ui.table.container>
    <div class="flex items-center">
        {{-- BULK ACTIONS --}}
        <div
            style="display:none;" 
            wire:show="selectedIds.length"
        >
            <x-ui.dropdown position="bottom-start">
                <x-slot:button class="justify-center">
                    <!-- desktop button -->
                    <x-ui.button 
                        icon="ellipsis-vertical" 
                        variant="soft"
                        size="sm"
                        class="
                            rounded-box mr-2 [@media(width<40rem)]:hidden outline 
                            dark:outline-white/20 outline-neutral-900/10 
                            dark:ring-white/15 ring-neutral-900/15 
                            [[data-open]>&]:bg-white/5 [[data-open]>&]:ring-2 shadow-sm
                        " 
                    >
                        Bulk Actions
                    </x-ui.button>
                    
                    <!-- mobile button (icon only) -->
                    <x-ui.button 
                        icon="ellipsis-vertical" 
                        variant="soft"
                        size="sm"
                        class="
                            rounded-box mr-2 sm:hidden outline dark:outline-white/20
                            outline-neutral-900/10 dark:ring-white/15 ring-neutral-900/15 
                            [[data-open]>&]:bg-white/5 [[data-open]>&]:ring-2 shadow-sm
                        " 
                    />
                </x-slot:button>
                
                <x-slot:menu>
                    <x-ui.dropdown.item 
                        icon="arrow-down-on-square"
                        wire:click="exportSelected"
                    >
                        Export Selected CSV
                    </x-ui.dropdown.item>
                    
                    <x-ui.dropdown.item 
                        icon="trash"
                        variant="danger"
                        wire:click="deleteSelected"
                        wire:confirm="Are you sure you want to delete?"
                    >
                        Delete Selected
                    </x-ui.dropdown.item>
                </x-slot:menu>
            </x-ui.dropdown>
        </div>

        {{-- SEARCH INPUT --}}
        <div class="ml-auto">
            <x-ui.input 
                class="[&_input]:bg-transparent"
                placeholder="Search..." 
                leftIcon="magnifying-glass" 
                wire:model.live="searchQuery"
            />
        </div>
    </div>
    
    <x-ui.table ...>
        <!-- table content... -->
    </x-ui.table>
</x-ui.table.container>

Add Column Visibility

Let's make status and difficulty hideable columns in our theorems table. Here's how:

#ID
Theorem
Mathematician
Field
Year
Difficulty
Status
1 Fundamental Theorem of Calculus Isaac Newton & Gottfried Leibniz 1666 7
2 Fermat's Last Theorem Andrew Wiles 1995 10
<x-ui.table.container x-data="{ hiddenCols: ['status', 'difficulty'] }">
    <div class="flex items-center">
        <!-- BULK ACTIONS CONTENT... -->
        <!-- SEARCH INPUT CONTENT... -->

        {{-- HIDDEN COLUMNS --}}
        <x-ui.dropdown
            checkbox
            checkboxVariant
            position="bottom-end"
        >
            <x-slot:button>
                <x-ui.button 
                    icon="view-columns" 
                    variant="soft"
                    size="sm"
                    class="rounded-box ml-2 outline dark:outline-white/20 outline-neutral-900/10 dark:ring-white/15 ring-neutral-900/15 [[data-open]>&]:bg-white/5 [[data-open]>&]:ring-2 shadow-sm" 
                />
            </x-slot:button>
            
            <x-slot:menu>                        
                <x-ui.dropdown.item readOnly>
                    Hidden Columns
                </x-ui.dropdown.item> 
                <x-ui.dropdown.separator/> 
                <x-ui.dropdown.item value="difficulty" x-model="hiddenCols">
                    Difficulty
                </x-ui.dropdown.item> 
                <x-ui.dropdown.item value="status" x-model="hiddenCols">
                    Status
                </x-ui.dropdown.item> 
            </x-slot:menu>
        </x-ui.dropdown>
    </div>
    
    <x-ui.table 
        :paginator="$theorems"
        pagination:variant="full"
        loadOn="pagination, search, sorting"
    >
        <x-ui.table.header sticky class="dark:bg-neutral-900 bg-white">
            <x-ui.table.columns withCheckAll>
                <x-ui.table.head sticky class="dark:bg-neutral-900 bg-white">
                    #ID
                </x-ui.table.head>
                <x-ui.table.head>Theorem</x-ui.table.head>
                <x-ui.table.head
                    column="mathematician"
                    sortable
                    :currentSortBy="$sortBy"
                    :currentSortDir="$sortDir"
                >
                    Mathematician
                </x-ui.table.head>
                <x-ui.table.head>Field</x-ui.table.head>
                <x-ui.table.head
                    column="year_discovered"
                    sortable
                    variant="dropdown"
                    :currentSortBy="$sortBy"
                    :currentSortDir="$sortDir"
                >
                    Year
                </x-ui.table.head>
                <x-ui.table.head
                    column="difficulty_level"
                    sortable
                    :currentSortBy="$sortBy"
                    :currentSortDir="$sortDir"
                    x-show="!hiddenCols.includes('difficulty')"
                >
                    Difficulty
                </x-ui.table.head>
                <x-ui.table.head  
                    x-show="!hiddenCols.includes('status')"
                >
                    Status
                </x-ui.table.head>
            </x-ui.table.columns>
        </x-ui.table.header>

        <x-ui.table.rows>
            @forelse($theorems as $theorem)
                <x-ui.table.row 
                    :checkboxId="$theorem->id" 
                    :key="$theorem->id"
                >
                    <!-- ID, Theorem, Mathematician, Field, Year cells... -->
                    
                    <x-ui.table.cell 
                        x-show="!hiddenCols.includes('difficulty')"
                    >
                        <!-- difficulty stars -->
                        <div class="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
                            Level {{ $theorem->difficulty_level }}
                        </div>
                    </x-ui.table.cell>
                    
                    <x-ui.table.cell  
                        x-show="!hiddenCols.includes('status')"
                    >
                        @if($theorem->is_proven)
                            <x-ui.badge icon="check-circle" color="green" size="sm" variant="outline">
                                Proven
                            </x-ui.badge>
                        @else
                            <x-ui.badge icon="exclamation-triangle" color="orange" size="sm" variant="outline">
                                Conjecture
                            </x-ui.badge>
                        @endif
                    </x-ui.table.cell>
                </x-ui.table.row>
            @empty
                <!-- empty state... -->
            @endforelse
        </x-ui.table.rows>
    </x-ui.table>
</x-ui.table.container>

Persisting Column Preferences

Use Alpine's $persist plugin to save user preferences across sessions:

x-data="{ 
    hiddenCols: $persist(['status', 'difficulty']).as('table-hidden-columns')
}"

This stores the user's column visibility choices in localStorage.


Adding Reordering

Two approaches: The guide below uses an in-memory $positions array — no database order column required, great for demos and simple lists. For production use with persistent ordering, see the Reorderable trait docs above which handles database shifting, scoping, and transactions for you.

<x-ui.table 
    reorderable
>
    <!-- table contents -->
</x-ui.table>

Each table row must expose its current order value. This value determines the row’s position.

<x-ui.table.rows>
    @forelse($users as $user)
        <x-ui.table.row 
            :checkboxId="$user->id" 
            :order="$user->order" 
            :key="$user->id"
        >
            <!-- ... -->
        </x-ui.table.row>
    @endforelse
</x-ui.table.rows>
<?php

declare(strict_types=1);

class Theorems extends Component
{
    // traits...

    #[Session()]
    public array $positions = [];

    public function mount()
    {
        if (empty($this->positions)) {
            $this->positions = $this->baseQuery()
                ->pluck('id')
                ->toArray();
        }
    }

    public function render(): View
    {
        $theorems = $this->baseQuery()
            ->when(filled($this->sortBy), function ($query) {
                return $this->applySorting($query);
            })
            ->when(filled($this->searchQuery), function ($query) {
                return $this->applySearch($query);
            })
            ->when(filled($this->positions), function ($query) {
                return $this->applyPositionSorting($query);
            })
            ->paginate($this->perPage);
        
        // more stuff

        return view('livewire.theorems', [
            'theorems' => $theorems,
        ]);
    }

   

    public function handleReordering($item, $position)
    {
        $itemId = (int) $item;
        // Remove item from current position
        $positions = array_values(array_filter($this->positions, fn ($id) => $id !== $itemId));
        // Insert at new position
        array_splice($positions, $position, 0, [$itemId]);
        $this->positions = $positions;
    }

    protected function applyPositionSorting(Builder $query): Builder
    {
        if (empty($this->positions)) {
            return $query;
        }
        $case = 'CASE';
        foreach ($this->positions as $index => $id) {
            $case .= " WHEN id = {$id} THEN {$index}";
        }
        $case .= ' END';
        return $query->orderByRaw($case);
    }

}

Guide's Full Code Source

Livewire/Theorems.php livewire class component

<?php

declare(strict_types=1);

namespace Src\Components\Livewire\Demos;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\View\View;
use Livewire\Attributes\Session;
use Livewire\Component;
use Livewire\Attributes\Renderless;
use Src\Components\Livewire\Concerns\CanExportCsv;
use Src\Components\Livewire\Concerns\WithPagination;
use Src\Components\Livewire\Concerns\WithSearch;
use Src\Components\Livewire\Concerns\WithSelection;
use Src\Components\Livewire\Concerns\WithSorting;
use Src\Components\Models\Theorem;

class Theorems extends Component
{
    use CanExportCsv;
    use WithPagination;
    use WithSearch;
    use WithSelection;
    use WithSorting;

    #[Session()]
    public array $positions = [];

    public function mount()
    {
        if (empty($this->positions)) {
            $this->positions = $this->baseQuery()
                ->pluck('id')
                ->toArray();
        }
    }

    public function render(): View
    {
        $theorems = $this->baseQuery()
            ->when(filled($this->sortBy), function ($query) {
                return $this->applySorting($query);
            })
            ->when(filled($this->searchQuery), function ($query) {
                return $this->applySearch($query);
            })
            ->when(filled($this->positions), function ($query) {
                return $this->applyPositionSorting($query);
            })
            ->paginate($this->perPage);

        $this->visibleIds = $theorems->pluck('id')->map(fn ($id) => (string) $id)->toArray();

        return view('livewire.theorems', [
            'theorems' => $theorems,
        ]);
    }

    #[Renderless]
    public function toCsv()
    {
        $theorems = $this->baseQuery();

        if (filled($this->searchQuery)) {
            $theorems = $this->applySearch($theorems);
        }

        if (filled($this->sortBy)) {
            $theorems = $this->applySorting($theorems);
        }

        if (filled($this->selectedIds)) {
            $theorems = $this->applySelection($theorems);
        }

        return $this->csv($theorems->get());
    }

    public function handleReordering($item, $position)
    {
        $itemId = (int) $item;

        // Remove item from current position
        $positions = array_values(array_filter($this->positions, fn ($id) => $id !== $itemId));

        // Insert at new position
        array_splice($positions, $position, 0, [$itemId]);

        $this->positions = $positions;
    }

   public function deleteSelected()
    {
        $this->baseQuery()
            ->whereIn('id', $this->selectedIds)
            ->delete();

        $this->deselectAll();
    }

    protected function applyPositionSorting(Builder $query): Builder
    {
        if (empty($this->positions)) {
            return $query;
        }

        $case = 'CASE';

        foreach ($this->positions as $index => $id) {
            $case .= " WHEN id = {$id} THEN {$index}";
        }

        $case .= ' END';

        return $query->orderByRaw($case);
    }

    protected function baseQuery(): Builder
    {
        return Theorem::query();
    }

    protected function applySearch($query)
    {
        return $query->where('name', 'like', '%'.$this->searchQuery.'%')
            ->orWhere('mathematician', 'like', '%'.$this->searchQuery.'%')
            ->orWhere('field', 'like', '%'.$this->searchQuery.'%')
            ->orWhere('statement', 'like', '%'.$this->searchQuery.'%');
    }
}

livewire/theorems.blade.php livewire blade component

<div class="min-h-screen py-12 px-4 ">
    <div class="md:sticky top-20 w-fit ">
        <x-ui.theme-switcher variant="inline"/>
    </div>
        
    <div class="max-w-6xl mx-auto space-y-8">
         <!-- Header -->
        <div class="text-center space-y-2 border border-dashed dark:border-white/15 border-neutral-950/35 py-6 rounded-box mb-20">
            <h1 class="text-3xl font-bold text-neutral-900 dark:text-white">
                Datatable Component
            </h1>
            <p class="text-neutral-600 dark:text-neutral-400">
                Interactive demos for Datatable component capabilitites
            </p>
        </div>

        <x-ui.table.container x-data="{ hiddenCols: ['status', 'difficulty'] }">
            <div 
                class="flex items-center"
            >
                {{-- BULK ACTIONS --}}
                <div
                    style="display:none;" 
                    wire:show="selectedIds.length"
                >
                    <x-ui.dropdown position="bottom-start">
                        <x-slot:button class="justify-center">
                            <x-ui.button 
                                icon="ellipsis-vertical" 
                                variant="soft"
                                size="sm"
                                class="rounded-box mr-2 [@media(width<40rem)]:hidden outline dark:outline-white/20 outline-neutral-900/10 dark:ring-white/15 ring-neutral-900/15 [[data-open]>&]:bg-white/5 [[data-open]>&]:ring-2 shadow-sm" 
                            >
                                bulk action
                            </x-ui.button>
                            
                            <x-ui.button 
                                icon="ellipsis-vertical" 
                                variant="soft"
                                size="sm"
                                class="rounded-box mr-2 sm:hidden outline dark:outline-white/20 outline-neutral-900/10 dark:ring-white/15 ring-neutral-900/15 [[data-open]>&]:bg-white/5 [[data-open]>&]:ring-2 shadow-sm" 
                            />
                        </x-slot:button>
                        
                        <x-slot:menu>
                            <x-ui.dropdown.item 
                                icon="arrow-down-on-square"
                                wire:click="toCsv"
                            >
                                export selected csv
                            </x-ui.dropdown.item>
                            
                            <x-ui.dropdown.item 
                                icon="trash"
                                variant="danger"
                                wire:click="deleteSelected"
                                wire:confirm="are you sure you want to delete ?"
                            >
                                delete selected
                            </x-ui.dropdown.item>
                        </x-slot:menu>
                    </x-ui.dropdown>
                </div>

                {{-- SEARCH INPUT --}}
                <div class="ml-auto">
                    <x-ui.input 
                        class="[&_input]:bg-transparent"  
                        placeholder="search..." 
                        leftIcon="magnifying-glass" 
                        wire:model.live="searchQuery"
                    />
                </div>

                {{-- FILTERS AND HIDDEN COLUMNS --}}
                <x-ui.dropdown
                    checkbox
                    checkboxVariant
                    position="bottom-end"
                >
                    <x-slot:button>
                        <x-ui.button 
                            icon="view-columns" 
                            variant="soft"
                            size="sm"
                            class="rounded-box ml-2 outline dark:outline-white/20 outline-neutral-900/10 dark:ring-white/15 ring-neutral-900/15 [[data-open]>&]:bg-white/5 [[data-open]>&]:ring-2 shadow-sm" 
                        />
                    </x-slot:button>
                    
                    <x-slot:menu>                        
                        <x-ui.dropdown.item readOnly>
                            hidden columns
                        </x-ui.dropdown.item> 
                        <x-ui.dropdown.separator/> 
                        <x-ui.dropdown.item 
                            x-model="hiddenCols" 
                        >
                            difficulty
                        </x-ui.dropdown.item> 
                        <x-ui.dropdown.item 
                            x-model="hiddenCols" 
                        >
                            status
                        </x-ui.dropdown.item> 
                    </x-slot:menu>
                </x-ui.dropdown>
                <x-ui.dropdown
                    checkbox
                    checkboxVariant
                    position="bottom-end"
                >
                    <x-slot:button>
                        <x-ui.button 
                            icon="funnel" 
                            variant="soft"
                            size="sm"
                            class="rounded-box ml-2 outline dark:outline-white/20 outline-neutral-900/10 dark:ring-white/15 ring-neutral-900/15 [[data-open]>&]:bg-white/5 [[data-open]>&]:ring-2 shadow-sm" 
                        />
                    </x-slot:button>
                    
                    <x-slot:menu>                        
                        <x-ui.dropdown.item readOnly>
                            Date Range
                        </x-ui.dropdown.item> 
                        <x-ui.dropdown.separator/> 
                        <x-ui.dropdown.item>
                            dificulty
                        </x-ui.dropdown.item> 
                    </x-slot:menu>
                </x-ui.dropdown>
            </div>
            <!-- Demo Table -->
            <x-ui.table 
                :paginator="$theorems"  
                pagination:variant="full"
                wire:loading
                reorderable
                loadOn="pagination, search, sorting"
            >
                <x-ui.table.header sticky class="dark:bg-neutral-900 bg-white">
                    <x-ui.table.columns withCheckAll>
                        <x-ui.table.head sticky class="dark:bg-neutral-900 bg-white">
                            #ID
                        </x-ui.table.head>
                        <x-ui.table.head>
                            Theorem
                        </x-ui.table.head>
                        <x-ui.table.head
                            column="mathematician"
                            sortable
                            :currentSortBy="$sortBy"
                            :currentSortDir="$sortDir"
                        >
                            Mathematician
                        </x-ui.table.head>
                        <x-ui.table.head>
                            Field
                        </x-ui.table.head>
                        <x-ui.table.head
                            column="year_discovered"
                            sortable
                            variant="dropdown"
                            :currentSortBy="$sortBy"
                            :currentSortDir="$sortDir"
                        >
                            Year
                        </x-ui.table.head>
                        <x-ui.table.head
                            column="difficulty_level"
                            sortable
                            :currentSortBy="$sortBy"
                            :currentSortDir="$sortDir"
                            x-show="!hiddenCols.includes('difficulty')" x-cloak
                        >
                            Difficulty
                        </x-ui.table.head>
                        <x-ui.table.head  
                            x-show="!hiddenCols.includes('status')" x-cloak
                        >
                            Status
                        </x-ui.table.head>
                    </x-ui.table.columns>
                </x-ui.table.header>

                <x-ui.table.rows>
                    @forelse($theorems as $theorem)
                        <x-ui.table.row 
                            :checkboxId="$theorem->id" 
                            :order="$theorem->id" 
                            :key="$theorem->id"
                        >
                            <x-ui.table.cell sticky class="[body:not(.sorting)_&]:dark:bg-neutral-950 [body:not(.sorting)_&]:bg-neutral-50">
                                {{ $theorem->id }}
                            </x-ui.table.cell>
                            <x-ui.table.cell>
                                <div class="max-w-xs">
                                    <div class="font-medium text-neutral-900 dark:text-neutral-100">
                                        {{ $theorem->name }}
                                    </div>
                                    <div class="text-xs text-neutral-500 dark:text-neutral-400 mt-1 line-clamp-2">
                                        {{ $theorem->statement }}
                                    </div>
                                </div>
                            </x-ui.table.cell>
                            
                            <x-ui.table.cell>
                                <div class="text-sm text-neutral-700 dark:text-neutral-300">
                                    {{ $theorem->mathematician }}
                                </div>
                            </x-ui.table.cell>
                            
                            <x-ui.table.cell>
                                @php
                                    $fieldColors = [
                                        'Number Theory' => 'purple',
                                        'Analysis' => 'blue',
                                        'Geometry' => 'green',
                                        'Algebra' => 'red',
                                        'Topology' => 'orange',
                                        'Probability' => 'pink',
                                        'Complex Analysis' => 'cyan',
                                        'Functional Analysis' => 'indigo',
                                        'Vector Calculus' => 'teal',
                                        'Game Theory' => 'violet',
                                        'Graph Theory' => 'lime',
                                        'Logic' => 'amber',
                                        'Linear Algebra' => 'rose',
                                        'Set Theory' => 'fuchsia',
                                        'Mathematical Physics' => 'sky',
                                        'Complexity Theory' => 'emerald',
                                    ];
                                    $color = $fieldColors[$theorem->field] ?? 'neutral';
                                @endphp
                                <x-ui.badge :color="$color" size="sm" variant="outline">
                                    {{ $theorem->field }}
                                </x-ui.badge>
                            </x-ui.table.cell>
                            
                            <x-ui.table.cell>
                                <div class="text-sm font-mono text-neutral-600 dark:text-neutral-400">
                                    {{ $theorem->year_discovered < 0 ? abs($theorem->year_discovered) . ' BC' : $theorem->year_discovered }}
                                </div>
                            </x-ui.table.cell>
                            
                            <x-ui.table.cell 
                                x-show="!hiddenCols.includes('difficulty')" x-cloak
                            >
                                <div class="flex items-center gap-1">
                                    @for($i = 1; $i <= min($theorem->difficulty_level, 10); $i++)
                                        <svg class="size-3 {{ $i <= 3 ? 'text-green-500' : ($i <= 6 ? 'text-yellow-500' : 'text-red-500') }}" fill="currentColor" viewBox="0 0 20 20">
                                            <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
                                        </svg>
                                    @endfor
                                </div>
                                <div class="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
                                    Level {{ $theorem->difficulty_level }}
                                </div>
                            </x-ui.table.cell>
                            
                            <x-ui.table.cell  
                                x-show="!hiddenCols.includes('status')" x-cloak
                            >
                                @if($theorem->is_proven)
                                    <x-ui.badge icon="check-circle" color="green" size="sm" variant="outline">
                                        Proven
                                    </x-ui.badge>
                                @else
                                    <x-ui.badge icon="exclamation-triangle" color="orange" size="sm" variant="outline">
                                        Conjecture
                                    </x-ui.badge>
                                @endif
                            </x-ui.table.cell>
                        </x-ui.table.row>
                    @empty
                        <x-ui.table.empty>
                            <x-ui.empty>
                                <x-ui.empty.media>
                                    <x-ui.icon name="inbox" class="size-10" />
                                </x-ui.empty.media>
                                <x-ui.empty.contents>
                                    <h3 class="text-lg font-semibold">No theorems found</h3>
                                    <p class="text-sm text-neutral-500">
                                        The mathematical universe seems empty!
                                    </p>
                                </x-ui.empty.contents>
                            </x-ui.empty>
                        </x-ui.table.empty>
                    @endforelse
                </x-ui.table.rows>
            </x-ui.table>
        </x-ui.table.container>

    </div>
</div>

Design Cookbook

Customize the table's appearance with utility classes and variants.

Bordered Table

Product
Price
Stock
Widget A $29.99 45
Widget B $39.99 12
<x-ui.table class="border border-neutral-200 dark:border-neutral-800 rounded-lg">
    <!-- Table content... -->
</x-ui.table>

if you have dynamic data table, it recomended to add the border to the table container and let's the container manager the padding between pagination, filters, and the actual table data.

just add the border prop to the container, you may tweack it on the container source code, the code is yours

<x-ui.table.container border>
    <div 
        class="flex items-center"
    >
        <!-- dynamic filters... -->
    </div>

    <!-- Demo Table -->
    <x-ui.table >
    <!-- table contents -->
    </x-ui.table >
</x-ui.table.container>

Striped Rows

Name
Email
Alice Johnson alice@example.com
Bob Smith bob@example.com
Carol White carol@example.com
<x-ui.table.rows class="[&>tr:nth-child(odd)]:bg-neutral-50 dark:[&>tr:nth-child(odd)]:bg-neutral-900/50">
    <!-- Rows... -->
</x-ui.table.rows>

Hover Effects

Name
Status
Alice Johnson Active
Bob Smith Active
<x-ui.table.row class="hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors cursor-pointer">
    <!-- Cells... -->
</x-ui.table.row>

Compact Table

Name
Email
Alice Johnson alice@example.com
Bob Smith bob@example.com
<!-- Override cell padding for compact layout -->
<x-ui.table.cell class="py-2">
    {{ $theorem->name }}
</x-ui.table.cell>

Custom Loading States

<x-ui.table 
    :paginator="$theorems"
    wire:loading
    loadOn="pagination, search, sorting"
>
    <x-slot name="loading">
        <div class="flex items-center gap-2">
            <x-ui.icon.loading class="size-5" />
            <span class="text-sm">Loading data...</span>
        </div>
    </x-slot>
    
    <!-- Table content... -->
</x-ui.table>

Component Props

table

Prop Type Default Description
paginator Paginator|null null Laravel paginator instance
pagination:variant string 'full' Pagination style: full, simple, compact
loadOn string null Comma-separated list of actions to show loading: pagination, search, sorting
loading slot null Custom loading indicator
reorderable boolean false make the table reorderable
footer slot null Content below the table
wire:loading boolean false Enable Livewire loading states
wire:target string null Specific Livewire actions to track
class string '' Additional CSS classes

table.container

Prop Type Default Description
border boolean true Show container border
class string '' Additional CSS classes

table.header

Prop Type Default Description
sticky boolean false Make header stick to top while scrolling
class string '' Additional CSS classes

table.columns

Prop Type Default Description
withCheckAll boolean false Add "select all" checkbox in header

table.head

Prop Type Default Description
column string|null null Column name for sorting
sortable boolean false Enable sorting for this column
variant string 'default' Sorting UI: default, dropdown
currentSortBy string '' Current sort column
currentSortDir string '' Current sort direction
sticky boolean false Make column stick while scrolling horizontally
class string '' Additional CSS classes

table.row

Prop Type Default Description
key string|null null Unique key for Livewire tracking
checkboxId string|int|null null ID for checkbox selection
order string|int|null null Current position value used for drag-and-drop reordering. Required when reorderable is enabled on the parent table.
class string '' Additional CSS classes

table.cell

Prop Type Default Description
sticky boolean false Make cell stick while scrolling horizontally
class string '' Additional CSS classes

Common Patterns

Reset All Filters

public function resetAll()
{
    $this->searchQuery = '';
    $this->sortBy = '';
    $this->sortDir = 'asc';
    $this->selectedIds = [];
    $this->resetPage();
}

Export All vs Selected

public function export($exportType = 'selected')
{
    $query = $this->baseQuery();
    
    if ($exportType === 'selected' && filled($this->selectedIds)) {
        $query->whereIn('id', $this->selectedIds);
    }
    
    // Apply current filters/search
    if (filled($this->searchQuery)) {
        $query = $this->applySearch($query);
    }

    .....
    
    return $this->csv($query->get());
}

Troubleshooting

Checkboxes Not Syncing

Ensure you're setting visibleIds in your render method:

public function render()
{
    $theorems = Theorem::query()->paginate($this->perPage);
    
    // This is crucial for checkbox sync
    $this->visibleIds = $theorems->pluck('id')
        ->map(fn ($id) => (string) $id)
        ->toArray();
        
    return view('livewire.theorems', ['theorems' => $theorems]);
}

Related Components

© SheafUI Copyright 2024-2026. All rights reserved.