PHPackages                             humweb/inertia-table - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. humweb/inertia-table

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

humweb/inertia-table
====================

Inertia.js table component with sorting, filtering, searching, and multi-table support for Laravel

3.0(1y ago)24.5k1[4 PRs](https://github.com/humweb/inertia-table/pulls)MITPHPPHP ^8.2CI passing

Since Jan 13Pushed 2w ago1 watchersCompare

[ Source](https://github.com/humweb/inertia-table)[ Packagist](https://packagist.org/packages/humweb/inertia-table)[ Docs](https://github.com/humweb/inertia-table)[ GitHub Sponsors](https://github.com/humweb)[ RSS](/packages/humweb-inertia-table/feed)WikiDiscussions main Synced today

READMEChangelog (6)Dependencies (16)Versions (14)Used By (0)

Inertia Table
=============

[](#inertia-table)

[![run-tests](https://github.com/humweb/inertia-table/actions/workflows/run-tests.yml/badge.svg)](https://github.com/humweb/inertia-table/actions/workflows/run-tests.yml)

Server-driven data tables for Laravel + Inertia.js + Vue 3. Define your columns, filters, sorts, and search on the backend — the frontend renders it all automatically with per-table partial reloads.

Installation
------------

[](#installation)

```
composer require humweb/inertia-table
```

Publish the config (optional):

```
php artisan vendor:publish --tag="inertia-table-config"
```

Quick Start
-----------

[](#quick-start)

### 1. Define a Resource

[](#1-define-a-resource)

A Resource declares your table's columns, filters, model, and query behavior:

```
use Humweb\Table\Resource;
use Humweb\Table\Fields\{FieldCollection, ID, Text, Badge};
use Humweb\Table\Filters\{FilterCollection, SelectFilter, TextFilter};

class UserResource extends Resource
{
    protected string $model = User::class;
    public string|Sort $defaultSort = 'name';
    protected array $with = ['team'];

    public function fields(): FieldCollection
    {
        return FieldCollection::make([
            ID::make('ID')->sortable(),
            Text::make('Name')->sortable()->searchable(),
            Text::make('Email')->sortable()->searchable(),
            Badge::make('Status')->sortable()->withMeta([
                'map' => [
                    'active' => ['label' => 'Active', 'class' => 'badge-green'],
                    'inactive' => ['label' => 'Inactive', 'class' => 'badge-gray'],
                ],
            ]),
        ]);
    }

    public function filters(): FilterCollection
    {
        return FilterCollection::make([
            SelectFilter::make('status', 'Status', [
                'active' => 'Active',
                'inactive' => 'Inactive',
            ]),
            TextFilter::make('name', 'Name'),
        ]);
    }
}
```

### 2. Use in a Controller

[](#2-use-in-a-controller)

#### Single table

[](#single-table)

```
use Inertia\Inertia;

class UserController extends Controller
{
    public function index(Request $request)
    {
        return Inertia::render('Users/Index')
            ->table(fn (InertiaTable $table) =>
                UserResource::make($request)->toResponse($table)
            );
    }
}
```

#### Multiple tables on one page

[](#multiple-tables-on-one-page)

```
public function index(Request $request)
{
    return Inertia::render('Staff/Teams/Show', [
        'team' => $team,
    ])
        ->table('members', fn (InertiaTable $table) =>
            MemberResource::make($request)->toResponse($table)
        )
        ->table('invitations', fn (InertiaTable $table) =>
            InvitationResource::make($request)->toResponse($table)
        );
}
```

Each table is a lazy closure, so when the frontend does a partial reload targeting one table (e.g. `only: ['tables.members']`), only that table's query runs — the other stays untouched.

### 3. Frontend (Vue 3)

[](#3-frontend-vue-3)

#### Single table

[](#single-table-1)

```

import { DataTable } from '@/components/Table/v2'

```

#### Multiple tables

[](#multiple-tables)

```

import { DataTable } from '@/components/Table/v2'

```

#### Using the composable directly

[](#using-the-composable-directly)

```

import { useTable } from '@/components/Table/v2'

const members = useTable('members')
const invitations = useTable('invitations')

    {{ record.name }}

```

---

Backend API
-----------

[](#backend-api)

### Resource

[](#resource)

Extend `Humweb\Table\Resource` to define a table. Required methods:

MethodReturnsPurpose`fields()``FieldCollection`Column definitions`filters()``FilterCollection`Filter definitions (optional, defaults to empty)Key properties:

PropertyTypeDefaultPurpose`$model``string`—Eloquent model class`$defaultSort``string|Sort``'id'`Default sort column or Sort instance`$with``array``[]`Eager-loaded relationships`$primaryKey``string``'id'`Record identifier`$parameters``array``[]`Route parameters passed to custom filters#### Custom parameter filters

[](#custom-parameter-filters)

Define `filter{StudlyKey}($value)` methods on your resource. Parameters set via `addParameter()` auto-dispatch to these methods:

```
$resource->addParameter('team_id', $team->id);

// In resource:
public function filterTeamId($value): void
{
    $this->query->where('team_id', $value);
}
```

#### Custom global search

[](#custom-global-search)

Override `globalFilter()` to replace the default OR-across-searchable-fields behavior:

```
public function globalFilter($query, $value): void
{
    $query->where(function ($q) use ($value) {
        $q->where('name', 'ilike', "%{$value}%")
          ->orWhere('email', 'ilike', "%{$value}%");
    });
}
```

#### Runtime transforms

[](#runtime-transforms)

```
$resource->runtimeTransform(function ($record) {
    $record['full_name'] = $record['first_name'] . ' ' . $record['last_name'];
    return $record;
});
```

### Fields

[](#fields)

All fields extend `Humweb\Table\Fields\Field` and use the `make()` static constructor.

#### Available field types

[](#available-field-types)

ClassComponentPurpose`ID``id-field`Primary key`Text``text-field`Text column`Textarea``textarea-field`Long text`Number``number-field`Numeric`Date``date-field`Date/datetime`Boolean``boolean-field`True/false badge`Badge``badge-field`Status badge with map`Currency``currency-field`Formatted currency`Percent``percent-field`Progress bar`Image``image-field`Image thumbnail`Avatar``avatar-field`Round avatar`Link``link-field`Clickable link`Relation``relation-field`Related model link`Computed``computed-field`Server-computed value`Actions``action-field`Row action buttons#### Field modifiers

[](#field-modifiers)

```
Text::make('Name')
    ->sortable()                          // Enable server-side sorting (BasicSort)
    ->sortable(new PowerJoinSort('team', 'name'))  // Sort via relation
    ->sortable(new AggregateSort('posts', 'count')) // Sort by withCount
    ->sortableOnClient()                  // Client-side sort (no server round-trip)
    ->sortField('name_lower')             // Sort on a different column than display
    ->searchable()                        // Include in column search
    ->visible(false)                      // Hidden by default
    ->visibility(true)                    // Allow toggling visibility
    ->nullable()                          // Mark as nullable
    ->withMeta(['tooltip' => 'Full name'])    // Arbitrary metadata sent to frontend
```

### Filters

[](#filters)

All filters extend `Humweb\Table\Filters\Filter`.

ClassComponentPurpose`TextFilter``text-filter`Free text input`SelectFilter``select-filter`Dropdown select`BooleanFilter``boolean-filter`Yes/No/Any`DateRangeFilter``date-range-filter`From/to date picker`NumberRangeFilter``number-range-filter`Min/max number`EnumFilter``enum-filter`Enum value select`ScopeFilter``scope-filter`Named query scope`RelationshipFilter``relationship-filter`Filter by related model`EmptyNotEmptyFilter``empty-filter`Null/empty check`TrashedFilter``select-filter`Soft delete filter#### Filter modifiers

[](#filter-modifiers)

```
TextFilter::make('name', 'Name')
    ->exact()                 // Exact match instead of LIKE
    ->startsWith()            // LIKE 'value%'
    ->endsWith()              // LIKE '%value'
    ->fullSearch()            // LIKE '%value%' (default)
    ->relation('team', 'name') // Filter within a relationship
    ->rules('string|max:100') // Validation rules
```

### Sort Strategies

[](#sort-strategies)

Sorts implement `Humweb\Table\Sorts\Sort` and are passed to `->sortable()`:

ClassPurposeExample`BasicSort`Simple `ORDER BY` (default). Delegates to Power Joins for dotted paths.`->sortable()``PowerJoinSort`Sort by a column on a related model via Power Joins.`->sortable(new PowerJoinSort('author', 'name'))``AggregateSort`Sort by `withCount`, `withSum`, `withAvg`, etc.`->sortable(new AggregateSort('orders', 'sum', 'total'))``SubquerySort`Sort by an arbitrary subquery (escape hatch).`->sortable(new SubquerySort(fn ($q) => ...))``CallbackSort`Sort via a custom callback.`->sortable(new CallbackSort(fn ($q, $desc, $prop) => ...))``NullsLastSort`Sort with NULLs always at the bottom.`->sortable(new NullsLastSort())`#### Collection sorts (client-side on server)

[](#collection-sorts-client-side-on-server)

For sorts that require fetching all records and sorting in PHP (e.g. computed values):

ClassPurpose`BasicCollectionSort`Sort a collection with auto type detection`CallbackCollectionSort`Custom collection sort callback```
Text::make('Score')
    ->sortable(new BasicCollectionSort(SortType::Integer), SortMode::Collection)
```

### Query Pipeline

[](#query-pipeline)

The `Resource` builds queries through a `QueryPipeline` of discrete `QueryStage` objects. The default pipeline runs these stages in order:

1. `ApplyEagerLoads` — `$with` relationships
2. `ApplyDefaultSort` — fallback sort when no `?sort=` param
3. `ApplySorts` — user-requested sort from `?sort=` param
4. `ApplyGlobalSearch` — `?search[global]=` (OR across searchable fields)
5. `ApplyCustomFilters` — parameter-based `filter*()` methods
6. `ApplySearch` — per-column `?search[name]=`
7. `ApplyFilters` — `FilterCollection` application from `?filters[status]=`

#### Customizing the pipeline

[](#customizing-the-pipeline)

Override `pipeline()` in your resource to add, replace, or reorder stages:

```
protected function pipeline(QueryPipeline $pipeline): QueryPipeline
{
    // Add a custom stage before sorting
    $pipeline->before(ApplySorts::class, new MyCustomStage());

    // Replace the default global search
    $pipeline->replace(ApplyGlobalSearch::class, new MyGlobalSearch());

    // Add a stage after filters
    $pipeline->after(ApplyFilters::class, new ApplyTenantScope($this->tenantId));

    return $pipeline;
}
```

#### Creating custom stages

[](#creating-custom-stages)

Implement `QueryStage`:

```
use Humweb\Table\Pipeline\QueryStage;
use Humweb\Table\TableRequest;
use Illuminate\Database\Eloquent\Builder;

class ApplyTenantScope implements QueryStage
{
    public function __construct(private int $tenantId) {}

    public function handle(Builder $query, TableRequest $request, Closure $next): Builder
    {
        $query->where('tenant_id', $this->tenantId);

        return $next($query);
    }
}
```

### TableRequest

[](#tablerequest)

`TableRequest` wraps the HTTP request with table-key awareness. For the `default` key, params are unprefixed (`?sort=name`). For named keys, params are prefixed (`?members.sort=name`).

```
$tableRequest = new TableRequest($request, 'members');
$tableRequest->getSortParam();    // reads ?members.sort=
$tableRequest->getSearchParams(); // reads ?members.search[...]=
$tableRequest->getFilterParams(); // reads ?members.filters[...]=
$tableRequest->getPage();         // reads ?members.page=
$tableRequest->getPerPage();      // reads ?members.perPage=
```

### Multi-Table Response Macro

[](#multi-table-response-macro)

The `->table()` macro on `Inertia\Response` supports two signatures:

```
// Single table (key = 'default', prop = 'table')
->table(fn (InertiaTable $table) => ...)

// Named table (prop = 'tables.{key}')
->table('members', fn (InertiaTable $table) => ...)
->table('invitations', fn (InertiaTable $table) => ...)
```

Each table is registered as a lazy closure. On the initial page visit both resolve. On partial reloads (e.g. sorting/filtering), Inertia's `only` parameter ensures only the targeted table re-evaluates.

---

Frontend API
------------

[](#frontend-api)

All frontend code lives in `resources/js/components/Table/v2/`.

### `useTable(key?, options?)`

[](#usetablekey-options)

The core composable. Call it with a table key to bind to a specific table's data from the Inertia page props.

```
import { useTable } from '@/components/Table/v2'

const table = useTable('members', {
  debounceMs: 300,
  preserveScroll: true,
  additionalOnly: ['team'],
})
```

#### Options

[](#options)

OptionTypeDefaultPurpose`debounceMs``number``250`Debounce delay for search/filter changes`preserveScroll``boolean``true`Preserve scroll position on reload`additionalOnly``string[]``[]`Extra Inertia `only` keys to include in partial reloads#### Return value

[](#return-value)

PropertyTypeDescription`key``string`Table identifier`sort``Ref`Current sort (e.g. `'name'` or `'-name'`)`page``Ref`Current page`perPage``Ref`Items per page`columns``ComputedRef`All column definitions`visibleColumns``ComputedRef`Only visible columns`filters``ComputedRef`Filter definitions with values`search``ComputedRef`Search field state`hasGlobalSearch``ComputedRef`Whether global search is available`records``ComputedRef`Current records (client-sorted if applicable)`pagination``ComputedRef`Pagination metadata`isLoading``Ref`Request in-flight indicator#### Methods

[](#methods)

MethodSignatureDescription`handleSort``(attribute: string) => void`Cycle sort: null -&gt; asc -&gt; desc -&gt; null`updateFilter``(key: string | number, value: unknown) => void`Set a filter value`updateSearch``(key: string, value: unknown) => void`Set a column search value`updateGlobalSearch``(value: unknown) => void`Set global search value`enableSearch``(key: string) => void`Enable a column search field`removeSearch``(key: string) => void`Disable and clear a search field`setPage``(page: number) => void`Navigate to page`setPerPage``(perPage: number) => void`Change per-page (resets to page 1)`toggleColumnVisibility``(attribute: string, visible: boolean) => void`Show/hide a column`refresh``() => void`Force reload this table### `` Component

[](#datatable-component)

The main component. Initializes `useTable` and provides it to child components via `provide('table')`.

```

```

#### Props

[](#props)

PropTypeDefaultDescription`tableKey``string``'default'`Table key matching the backend`enableRowSelection``boolean``false`Show row checkboxes`selectionKey``string``'id'`Record property for selection identity`hideToolbar``boolean``false`Hide the toolbar`caption``string``''`Accessible table caption`ariaLabel``string``''`Accessible table label`options``UseTableOptions``{}`Options forwarded to `useTable`#### Slots

[](#slots)

SlotScopeDescription`toolbar``{ table }`Replace the entire toolbar`table``{ table, records }`Replace the entire table element`head``{ columns, sortHandler, sort }`Replace the ```body``{ records, columns }`Replace the ```cell:{attribute}``{ record, field }`Override a specific column cell`pagination``{ table }`Replace pagination### Sub-components

[](#sub-components)

All sub-components inject `useTable` via `inject('table')` and can be used standalone:

ComponentPurpose`TableToolbar`Search, filters, column visibility`TableHeader` / `TableHeaderCell`Sortable column headers`TableBody` / `TableBodyCell`Record rows with field rendering`TablePagination`Page navigation and per-page select`FieldRenderer`Resolves field component by `component` type`FilterRenderer`Resolves filter component by `component` type`GlobalSearch`Search input for global search`ColumnSearch`Active column search fields`ColumnSearchDropdown`Dropdown to enable column searches### Imports

[](#imports)

```
// Components
import { DataTable, TableHeader, TableBody, TablePagination } from '@/components/Table/v2'

// Composable
import { useTable } from '@/components/Table/v2'

// Types
import type { TableColumn, UseTableReturn, PaginationData } from '@/components/Table/v2'
```

---

Configuration
-------------

[](#configuration)

```
// config/inertia-table.php
return [
    'pagination' => [
        'max_per_page' => 100,
        'default_per_page' => 15,
    ],
];
```

Testing
-------

[](#testing)

```
composer test
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Credits
-------

[](#credits)

- [ryun](https://github.com/humweb)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

46

—

FairBetter than 92% of packages

Maintenance72

Regular maintenance activity

Popularity21

Limited adoption so far

Community11

Small or concentrated contributor base

Maturity66

Established project with proven stability

 Bus Factor1

Top contributor holds 75.2% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~137 days

Recently: every ~171 days

Total

6

Last Release

489d ago

Major Versions

v1.1 → 2.02023-08-13

1.5 → 3.02025-03-02

PHP version history (3 changes)v1.0PHP ^8.0

2.0PHP ^8.1

3.0PHP ^8.2

### Community

Maintainers

![](https://www.gravatar.com/avatar/8b536bae26e43306745d195a34b8dff510bc05419cdc3503f96c5cb3fd4e7239?d=identicon)[ryun](/maintainers/ryun)

---

Top Contributors

[![ryun](https://avatars.githubusercontent.com/u/227672?v=4)](https://github.com/ryun "ryun (191 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (34 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (29 commits)")

---

Tags

laravelhumwebinertia-table

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/humweb-inertia-table/health.svg)

```
[![Health](https://phpackages.com/badges/humweb-inertia-table/health.svg)](https://phpackages.com/packages/humweb-inertia-table)
```

###  Alternatives

[spatie/laravel-pdf

Create PDFs in Laravel apps

1.0k4.8M47](/packages/spatie-laravel-pdf)[codewithdennis/filament-select-tree

The multi-level select field enables you to make single selections from a predefined list of options that are organized into multiple levels or depths.

329530.5k29](/packages/codewithdennis-filament-select-tree)[filament/support

Core helper methods and foundation code for all Filament packages.

2331.0M245](/packages/filament-support)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3914.6k](/packages/rawilk-profile-filament-plugin)[worksome/exchange

Check Exchange Rates for any currency in Laravel.

124603.0k](/packages/worksome-exchange)[robertboes/inertia-breadcrumbs

Laravel package to automatically share breadcrumbs to Inertia

59150.6k1](/packages/robertboes-inertia-breadcrumbs)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
