Back to Home

Table

Powerful data table component with search, sorting, filtering, pagination, inline editing, bulk actions, and column management — built on Livewire.

Live Example

Here is the Table component in action with all its features:
Name
Email
Status
Created
Actions
John Doe
john.doe@example.com
active
2026-02-20
Jane Smith
jane.smith@example.com
pending
2026-02-22
Bob Johnson
bob.johnson@example.com
active
2026-02-24

Getting Started

Create a class that extends Neura\Kit\Components\Atoms\Table and implement the query() and columns() methods:
<?php

namespace App\View\Pages\Backend\Users;

use Neura\Kit\Components\Atoms\Table as BaseTable;
use Neura\Kit\Support\Table\Column;
use Illuminate\Database\Eloquent\Builder;
use App\Models\User;

class UserTable extends BaseTable
{
    public int $perPage = 10;

    public function query(): Builder
    {
        return User::query()->latest();
    }

    public function columns(): array
    {
        return [
            Column::text('name', 'Name')
                ->sortable()
                ->searchable(),

            Column::text('email', 'Email')
                ->sortable()
                ->searchable(),

            Column::humanDiff('created_at', 'Created')
                ->sortable(),
        ];
    }
}
Then render it in your Blade view:
<livewire:backend.users.user-table />

Style Packs

Customize the table appearance by overriding style methods. Each method returns a string key that maps to a design pack.
use Neura\Kit\Enum\Table\{Variant, Rounded, Shadow, Density};

class ProductTable extends BaseTable
{
    public function variant(): Variant { return Variant::STRIPED; }
    public function rounded(): Rounded { return Rounded::LG; }
    public function shadow(): Shadow   { return Shadow::MD; }
    public function density(): Density { return Density::COMPACT; }
    public function hoverable(): bool  { return true; }
    public function bordered(): bool   { return true; }   // false = no borders
    public function fullHeight(): bool { return false; }  // true = fill viewport, scroll body

    // ...
}
You can also use plain strings if you prefer:
public function variant(): string { return 'striped'; }
public function rounded(): string { return 'lg'; }

Variant

Controls the overall visual style — borders, backgrounds, and row separation. Override variant():
Value Description
default Clean bordered card with subtle header background (default)
striped Alternating row backgrounds for readability
minimal No wrapper border, transparent background
flat Filled background without border
bordered Thicker borders for emphasis
elevated Subtle borders designed to pair with larger shadows
use Neura\Kit\Enum\Table\Variant;

// Striped rows
public function variant(): Variant { return Variant::STRIPED; }

// Minimal — no wrapper border, transparent
public function variant(): Variant { return Variant::MINIMAL; }

// Elevated — pair with Shadow::LG or Shadow::XL
public function variant(): Variant { return Variant::ELEVATED; }

Rounded

Controls the border radius of the wrapper, toolbar, and footer. Override rounded():
Value CSS
none rounded-none
sm rounded-sm
md rounded-md
lg rounded-lg
xl rounded-xl (default)
2xl rounded-2xl

Shadow

Controls the box shadow depth. Override shadow():
Value Description
none No shadow
xs Minimal shadow
sm Subtle shadow (default)
md Medium depth
lg Prominent shadow
xl Maximum depth — floating card effect

Density

Controls padding and text size. Override density():
Value Cell Padding Text Size
compact px-2.5 py-1 text-xs
normal px-3 py-2 text-sm (default)
comfortable px-4 py-3 text-sm

bordered()

When bordered() returns false, all table borders (wrapper, rows, toolbar, footer) are removed. Backgrounds and hover styles are kept. Useful for embedding inside cards or when you want a borderless look.
public function bordered(): bool
{
    return false;  // borderless table
}

fullHeight()

When fullHeight() returns true, the table fills the available height of its container without exceeding it. The toolbar and footer stay fixed; only the table body scrolls. The parent must have a defined height (e.g. h-screen, or min-h-0 inside a flex layout).
use Neura\Kit\Enum\Table\Variant;

// Full-height table in a dashboard layout
public function fullHeight(): bool
{
    return true;
}

// In your Blade: wrap in a flex container with height
// <div class="flex flex-col h-[calc(100vh-4rem)] min-h-0">
//     <livewire:backend.users.user-table />
// </div>

Combining Packs

Mix and match packs for different use cases:
use Neura\Kit\Enum\Table\{Variant, Rounded, Shadow, Density};

// Dashboard analytics — compact, no rounded
class AnalyticsTable extends BaseTable
{
    public function variant(): Variant { return Variant::STRIPED; }
    public function rounded(): Rounded { return Rounded::NONE; }
    public function shadow(): Shadow   { return Shadow::NONE; }
    public function density(): Density { return Density::COMPACT; }
}

// Product catalog — elevated, prominent shadow
class ProductTable extends BaseTable
{
    public function variant(): Variant { return Variant::ELEVATED; }
    public function rounded(): Rounded { return Rounded::XL2; }
    public function shadow(): Shadow   { return Shadow::LG; }
    public function density(): Density { return Density::COMFORTABLE; }
}

// Embedded inside a card — minimal, no border
class LogTable extends BaseTable
{
    public function variant(): Variant { return Variant::MINIMAL; }
    public function rounded(): Rounded { return Rounded::NONE; }
    public function shadow(): Shadow   { return Shadow::NONE; }
    public function density(): Density { return Density::COMPACT; }
}
Live preview with Variant::STRIPED, Rounded::LG, Shadow::MD, and Density::COMPACT:
Product
Price
Stock
Updated
Widget A
$29.99
In stock
2026-02-28
Widget B
$49.99
Low stock
2026-02-25
Widget C
$19.99
Out of stock
2026-03-01

Real-World Example

A complete Teams table with inline editing, actions, bulk actions, filters, dialogs, modals, and empty state:
<?php

namespace App\View\Pages\Backend\Customer\Teams;

use App\Models\Team;
use App\View\Modals\Teams\{Create, Edit, Invite};
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Neura\Kit\Components\Atoms\Table as BaseTable;
use Neura\Kit\Concerns\InteractsWithNeuraKit;
use Neura\Kit\Enum\Table\{Variant, Rounded, Shadow, Density};
use Neura\Kit\Support\Table\{Action, Column, EmptyState};

class Table extends BaseTable
{
    use InteractsWithNeuraKit;

    public int $perPage = 10;

    public function variant(): Variant { return Variant::DEFAULT; }
    public function rounded(): Rounded { return Rounded::XL; }
    public function shadow(): Shadow   { return Shadow::SM; }
    public function density(): Density { return Density::NORMAL; }

    public function query(): Builder
    {
        return Team::accessibleByUser(Auth::user())
            ->with(['owner', 'members.user'])
            ->withCount('members')
            ->latest();
    }

    public function columns(): array
    {
        return [
            Column::text('name', 'Team name')
                ->editable()
                ->searchable()
                ->sortable(),

            Column::text('owner_name', 'Owner')
                ->sortable(),

            Column::text('members_count', 'Members')
                ->sortable(),

            Column::status('current_role', 'Your role')
                ->sortable()
                ->format(fn($state) => match ($state) {
                    'owner' => 'Owner',
                    'member' => 'Member',
                    default => 'Not member',
                })
                ->filterable('select', [
                    'owner' => 'Owner',
                    'member' => 'Member',
                ]),

            Column::humanDiff('created_at', 'Created at')
                ->sortable(),

            Column::actions('actions', 'Actions', $this->getColumnActions())
                ->resizable(false),
        ];
    }

    public function actions(): array
    {
        return [
            Action::make('Create team')
                ->icon('plus')
                ->variant('primary')
                ->wireClick('create')
                ->visible(fn() => $this->data()->total() > 0),
        ];
    }

    protected function getColumnActions(): array
    {
        return [
            Action::make('Invite user')
                ->icon('user-plus')
                ->wireClick('invite')
                ->variant('soft')
                ->visible(fn($team) => $team->canManage(Auth::user()))
                ->tooltip('Invite a user'),

            Action::make('Edit team')
                ->icon('pencil')
                ->wireClick('edit')
                ->variant('soft')
                ->visible(fn($team) => $team->canManage(Auth::user())),

            Action::make('Delete team')
                ->icon('trash')
                ->variant('danger-soft')
                ->wireClick('delete')
                ->visible(fn($team) => $team->canManage(Auth::user())),
        ];
    }

    public function bulkActions(): array
    {
        return [
            Action::make('Delete selected')
                ->icon('trash')
                ->variant('danger-ghost')
                ->wireClick('bulkDelete'),
        ];
    }

    public function create(): void
    {
        $this->modal(Create::class)->open();
    }

    public function edit($team): void
    {
        $this->modal(Edit::class)->with('team', $team)->open();
    }

    public function delete($team): void
    {
        $this->dialog('Delete team?')
            ->danger()
            ->message('This action cannot be undone.')
            ->confirmText('Delete team')
            ->onConfirm('confirmDelete', $team)
            ->show();
    }

    public function emptyState(): EmptyState
    {
        return EmptyState::make()
            ->message('No teams found')
            ->wireClick('Create your first team', 'create');
    }
}

Column Types

All column types available via the Column class:
Factory Method Description
Column::text($key, $label) Plain text value
Column::date($key, $label, $format?) Formatted date
Column::humanDiff($key, $label) Relative time (e.g. "2 hours ago")
Column::boolean($key, $label) True/false indicator
Column::status($key, $label, $enum?, $colors?) Colored status badge
Column::badgeColumn($key, $label, $options?) Configurable badge (colors, icons, variants)
Column::belongsTo($key, $label, $model, $attr) BelongsTo relation display
Column::relation($key, $label, $rel, $attr) Simple relation attribute
Column::relationCount($key, $label, $rel, $popover?, $attr?) Relation count with optional popover
Column::increment($key, $label) Auto-incrementing row number
Column::image($key, $label) Image thumbnail
Column::avatar($key, $label, $nameKey?) Rounded avatar image
Column::icon($key, $label) Dynamic icon from value
Column::color($key, $label) Color swatch
Column::link($key, $label, $url?) Clickable link
Column::email($key, $label) Email link (copyable)
Column::phone($key, $label) Phone link (copyable)
Column::money($key, $label, $currency?) Formatted currency value
Column::percentage($key, $label, $decimals?) Percentage value
Column::array($key, $label) Array of values
Column::tags($key, $label, $colors?) Colored tag list
Column::count($key, $label) Count value
Column::avg($key, $label) Average value
Column::htmlContent($key, $label) Raw HTML content
Column::userType($key, $label) User type display
Column::actions($key, $label, $actions) Row action buttons

Column Options

Fluent methods available on every column instance:
Method Description
sortable() Enable column sorting
searchable() Include in global search
filterable($type?, $options?, $query?) Add filter (text, select, date, boolean)
editable($type?, $options?) Enable inline editing (text, number, date, select, textarea, boolean)
width($w, $min?, $max?) Set width in pixels
resizable($bool?) Enable column resizing (default: true)
format(Closure $cb) Format value with callback
formatUsing($format) Predefined format (date format, number, currency)
copyable() Show copy-to-clipboard on hover
truncate($bool?, $length?) Truncate text with ellipsis
align($align) Text alignment (start, center, end)
placeholder($text) Placeholder when value is empty
tooltip($text|Closure) Show tooltip on hover
visible($bool|Closure) Control column visibility
hidden() Hide column (shortcut for visible(false))

Inline Editing

Make cells editable inline (click to edit, Enter to save, Escape to cancel). The updateField() method on the base Table class handles persistence automatically.
// Text (default)
Column::text('name', 'Name')->editable(),

// Specific type
Column::text('price', 'Price')->editable('number'),
Column::text('bio', 'Bio')->editable('textarea'),
Column::date('due_date', 'Due')->editable(),

// Select with options
Column::text('status', 'Status')->editable('select', [
    'active' => 'Active',
    'draft' => 'Draft',
    'archived' => 'Archived',
]),

// Boolean (toggles on click)
Column::boolean('is_active', 'Active')->editable(),
Editable types: text, number, date, select, textarea, boolean. A pencil icon appears on row hover to indicate editability.
To customize persistence logic, override updateField() in your table class:
public function updateField(string|int $rowId, string $column, mixed $value): void
{
    // Custom validation
    if ($column === 'price' && $value < 0) {
        $this->addError('field', 'Price cannot be negative');
        return;
    }

    // Call parent for default persistence
    parent::updateField($rowId, $column, $value);

    // Or handle it entirely yourself
    $model = Product::findOrFail($rowId);
    $model->update([$column => $value]);

    $this->dispatch('notify', message: 'Updated successfully');
}

Actions

Actions use the Action class for a fluent API. There are three types: toolbar actions, column (row) actions, and bulk actions.

Toolbar Actions

Displayed in the table toolbar. Override actions():
public function actions(): array
{
    return [
        Action::make('Create team')
            ->icon('plus')
            ->variant('primary')
            ->wireClick('create'),

        Action::make('Export')
            ->icon('arrow-down-tray')
            ->variant('outline')
            ->wireClick('export'),
    ];
}

Row Actions

Passed to Column::actions(). Each action can be conditionally visible per row:
Column::actions('actions', 'Actions', [
    Action::make('Edit')
        ->icon('pencil')
        ->wireClick('edit')
        ->variant('soft')
        ->tooltip('Edit this item'),

    Action::make('Delete')
        ->icon('trash')
        ->variant('danger-soft')
        ->wireClick('delete')
        ->visible(fn($row) => $row->canDelete()),
])->resizable(false),

Bulk Actions

Override bulkActions(). When rows are selected, a banner shows the count and bulk action buttons appear in the toolbar:
public function bulkActions(): array
{
    return [
        Action::make('Delete selected')
            ->icon('trash')
            ->variant('danger-ghost')
            ->wireClick('bulkDelete'),

        Action::make('Export selected')
            ->icon('arrow-down-tray')
            ->variant('soft')
            ->wireClick('bulkExport'),
    ];
}

Action API

Method Description
Action::make($label) Create a new action
->icon($name) Set Heroicon name
->variant($variant) Style: primary, soft, danger-soft, danger-ghost, outline, ghost
->wireClick($method) Livewire method to call (row ID passed automatically)
->route($name, $params?) Navigate to a named route
->url($url) Navigate to a URL
->tooltip($text) Show tooltip on hover
->visible(Closure|bool) Conditionally show/hide (receives $row for column actions)
->size($size) Button size (sm, md, lg)

Filtering

Add filterable columns with different filter types. Filters appear as inline inputs in the toolbar:
// Text filter (default)
Column::text('name', 'Name')->filterable(),

// Select filter
Column::text('status', 'Status')->filterable('select', [
    'active' => 'Active',
    'inactive' => 'Inactive',
]),

// Boolean filter
Column::boolean('is_active', 'Active')->filterable('boolean'),

// Date filter
Column::date('created_at', 'Created')->filterable('date'),

// Custom filter query
Column::text('name', 'Name')->filterable('select', $options, function ($query, $value) {
    $query->where('name', 'like', "%{$value}%");
}),

Empty State

Customize the message shown when no rows are found. Override emptyState():
use Neura\Kit\Support\Table\EmptyState;

public function emptyState(): EmptyState
{
    return EmptyState::make()
        ->message('No teams found')
        ->wireClick('Create your first team', 'create');
}

Table Properties

Public properties available on the base Table class:
Property Type Default Description
$perPage int 10 Rows per page
$sortBy string '' Current sort column key
$sortDirection string 'asc' Sort direction (asc or desc)
$search string '' Global search term
$filters array [] Active filter values keyed by column
$selected array [] Selected row IDs for bulk actions

Overridable Methods

Methods you can override to customize behavior:
Method Returns Description
query() Builder Base query (required)
columns() array Column definitions (required)
actions() array Toolbar action buttons
bulkActions() array Bulk action buttons
emptyState() EmptyState|string|null Empty state configuration
variant() Variant|string Table visual variant
rounded() Rounded|string Border radius size
shadow() Shadow|string Box shadow depth
density() Density|string Padding and text size
hoverable() bool Enable row hover highlight
bordered() bool Show borders on wrapper, rows, toolbar, footer (default: true). Return false for borderless.
fullHeight() bool Fill container height with internal scroll (default: false). Parent must have defined height.
getRowKey() string Primary key column (default: 'id')
updateField() void Handle inline edit persistence
refreshTable() void Reset cache, selection, and optionally page

File Structure

The table component is organized into modular sub-components:
table/
├── index.blade.php              # Main orchestrator
├── columns/                     # Column type templates
│   ├── column.blade.php         # Default text column
│   ├── boolean.blade.php        # Boolean indicator
│   ├── status.blade.php         # Status badge
│   ├── badge.blade.php          # Configurable badge
│   ├── date.blade.php           # Formatted date
│   ├── human-diff.blade.php     # Relative time
│   ├── image.blade.php          # Image thumbnail
│   ├── icon.blade.php           # Dynamic icon
│   ├── color.blade.php          # Color swatch
│   ├── link.blade.php           # Clickable link
│   ├── actions.blade.php        # Row action buttons
│   └── ...                      # Other column types
└── parts/                       # Structural sub-components
    ├── toolbar.blade.php        # Search, filters, actions
    ├── header.blade.php         # <thead> with sorting & resizing
    ├── row.blade.php            # <tr> with bulk checkbox
    ├── cell.blade.php           # <td> with inline editing
    ├── bulk-banner.blade.php    # Selected rows banner
    ├── empty.blade.php          # Empty state display
    ├── pagination.blade.php     # Pagination links
    ├── filter-input.blade.php   # Filter input controls
    └── action.blade.php         # Action button template

Features

Search

Global search across searchable() columns, debounced.

Sorting

Click column headers to sort asc/desc.

Filtering

Text, select, date, and boolean filter types.

Inline Editing

Click cells to edit in-place. Enter saves, Escape cancels.

Column Visibility

Toggle columns on/off from toolbar dropdown.

Column Resizing

Drag column edges to resize. Widths persist via Livewire.

Bulk Actions

Select rows and perform batch operations.

Style Packs

Variant, rounded, shadow, density — composable design tokens.

Pagination

Automatic with configurable $perPage.

Empty State

Customizable message and CTA when no rows match.

Best Practices

✓ Do

  • Eager load relations with ->with()
  • Only mark relevant columns as searchable()
  • Use select filters for enum-like values
  • Set explicit widths on columns that need it
  • Use resizable(false) on actions columns
  • Choose density('compact') for data-heavy tables
  • Use variant('minimal') when embedded in cards

✗ Don't

  • Make all columns searchable unnecessarily
  • Forget with() causing N+1 queries
  • Show too many columns by default
  • Use heavy queries without indexes
  • Forget to set $perPage for large datasets
  • Mix variant('elevated') with shadow('none')
  • Use large rounded values inside a flat container