Table
Live Example
|
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
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(),
];
}
}
<livewire:backend.users.user-table />
Style Packs
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
// ...
}
public function variant(): string { return 'striped'; }
public function rounded(): string { return 'lg'; }
Variant
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
rounded():
| Value | CSS |
|---|---|
| none | rounded-none |
| sm | rounded-sm |
| md | rounded-md |
| lg | rounded-lg |
| xl | rounded-xl (default) |
| 2xl | rounded-2xl |
Shadow
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
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()
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()
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
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; }
}
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
<?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
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
| 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
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(),
text, number, date, select, textarea, boolean. A pencil icon appears on row hover to indicate editability.
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
Action class for a fluent API. There are three types: toolbar actions, column (row) actions, and bulk actions.
Toolbar Actions
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
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
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
// 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
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
| 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
| 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
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
$perPagefor large datasets - Mix
variant('elevated')withshadow('none') - Use large rounded values inside a flat container