Tables that
speak Laravel.
Define tables with expressive PHP classes and render them with our first-party Vue and Inertia adapters.

The capability ledger
Every row, accounted for.
Zonvoir Table's own feature set, kept in the same format your users will see it in.
One class, one source of truth
The query, columns and payload - defined once.
A table class drives the Eloquent query, the columns, URL state, row actions, and exports. The Vue adapter just renders what it's given.
1 use App\Models\User;
2 use Zonvoir\ZonvoirTable\Columns\BadgeColumn;
3 use Zonvoir\ZonvoirTable\Columns\TextColumn;
4 use Zonvoir\ZonvoirTable\Export;
5 use Zonvoir\ZonvoirTable\Table;
6
7 final class UsersTable extends Table
8 {
9 protected ?string $resource = User::class;
10 protected array|string|null $search = ['name', 'email'];
11 protected ?string $defaultSort = 'name';
12
13 public function columns(): array
14 {
15 return [
16 TextColumn::make('name', 'Full Name')->searchable(),
17 TextColumn::make('email')->searchable(),
18 ];
19 }
20 }
Features
Built for every row. Ready for every workflow.
# Built-in Table Definition
One PHP class drives the query, columns, URL state, row actions, exports, and frontend metadata.
- Resolve an Eloquent model class or query builder.
- Return typed column, action, and export definitions.
- Serialize a stable payload for Inertia.
UsersTable::make();# Search State
Search can be table-level, column-level, or customized with callbacks in the query builder layer.
- Set searchable fields on the table.
- Mark individual columns as searchable.
- Keep search state in the query string.
protected array|string|null $search = ['name', 'email'];
TextColumn::make(‘name’)->searchable();
# Sorting
Sortable columns expose direction-aware query state while the backend stays in charge of the query.
- Enable sorting per column.
- Use `-column` for descending defaults.
- Override sorting with `sortUsing()` when needed.
protected ?string $defaultSort = '-created_at';
TextColumn::make(‘name’)->sortable();
# Pagination
Pagination configuration lives with the table, including allowed page sizes and pagination strategy.
- Standard, simple, and cursor modes.
- Configurable per-page options.
- Normalized page, cursor, and per-page input.
protected ?int $defaultPerPage = 30;
protected ?array $perPageOptions = [15, 30, 50];# Row Actions & Links
Actions and row URLs are serialized as metadata so Vue can render the right controls.
- Return actions from `actions()`.
- Use `Url` for route and Inertia metadata.
- Support authorization, disabled, hidden, and confirm states.
Action::make('Edit')
->url(fn ($user, $url) => $url->route('users.edit', $user));# Bulk Actions
Bulk actions reuse the action API and execute against selected row keys.
- Enable bulk mode with `asBulkAction()`.
- Use `onlyAsBulkAction()` for bulk-only flows.
- Control chunk size and strategy.
Action::make('Archive')
->asBulkAction(chunkSize: 100)
->confirm('Archive users?');# Columns
One Columns guide covers common options plus text, numeric, boolean, badge, image, date, date-time, and action columns.
- Shared visibility, sizing, sticky, search, and sort metadata.
- Column-specific display settings.
- Export labels, values, formats, and styles.
BadgeColumn::make('status')
->colors(['active' => 'success'])
->solid();# Sticky Columns
Sticky columns keep high-value identifiers visible while users scan wide data sets.
- Enable sticky behavior per column.
- Pair with width metadata for predictable layout.
- Allow sticky overrides through table state.
TextColumn::make('name')
->sticky()
->width('16rem');# Excel Exports
Return export definitions from the table and choose filtered or selected-row export scope.
- Expose export controls in table metadata.
- Limit to filtered or selected rows.
- Queue large exports when needed.
Export::make('Users', 'users.xlsx')
->limitToFilteredRows();# URL State
Table state is represented in query string keys for page, per-page, cursor, search, sort, direction, columns, and sticky overrides.
- Name tables to namespace query state.
- Generate navigation URLs from the current state.
- Keep reloads and browser history predictable.
$table->navigation()->search('olivia');
$table->navigation()->sort('name');# Multiple Tables
Name each table to avoid query string collisions when a page renders more than one table.
- Use `named()` or `as()`.
- Separate page, search, sort, columns, and sticky state.
- Render multiple independent table payloads.
UsersTable::make()->named('users')->results();
OrdersTable::make()->named('orders')->results();Start with a real table
Create a table, render it, then add behavior feature by feature.
Understand the moving parts
Columns, state, row links, selection, pagination, and payload shape.
Check exact signatures
Classes, methods, enum cases, callback signatures, and Vue exports.
