Inertia Table
A powerful data table component.
By purchasing Inertia Start, you also get a license for Inertia Table, our premium Vue.js data table component built on top of TanStack Table v9.
If you don't need the full starter kit, you can also buy Inertia Table separately.
Buying Inertia Table separately
If you start with a standalone Inertia Table license and later decide you want the full starter kit, you’ll automatically get 25% off Inertia Start. See Upgrading to Inertia Start below.
Sort, filter, paginate and toggle columns on a live table with real data.
Capabilities
- Sorting: Click a column header to cycle through ascending, descending and unsorted. Right-click (or long-press on touch devices) opens a context menu with explicit Asc, Desc, Remove sorting and Hide actions. Sorting happens server-side through a
sortquery param, so it applies to the whole dataset and not only the current page. Enable it per column withenableSortingin yourInertiaTableColumnDef, set an initial order with thedefaultSortingprop, and allow sorting on several columns at once withenableMultiSort. - Filtering: Two complementary ways to narrow results, both sent server-side as
filter[...]query params. Thesearchprop renders a text input for free-text search, and thefiltersprop renders faceted dropdowns with predefined options. Each filtered attribute must be declared inallowedFilterson the backend. - Pagination: Built on Laravel Pagination — pass a paginator (or an API resource collection) and the table renders page links and a page-size selector, driven by the
pageandper_pagequery params. Customize the available page sizes with thepageSizesprop. - Visibility: A dropdown lets users show or hide columns, so dense tables stay readable on smaller screens. Mark a column as hideable with
enableHidingin itsInertiaTableColumnDef; the toggle itself is controlled by theenableColumnToggleprop. - Selectable: Set
enableRowSelectionto add checkboxes to each row plus a "select all" checkbox in the header. Access the selected rows from the parent component through the exposed TanStacktableinstance (getSelectedRowModel()) to build your own bulk actions such as deleting or exporting. - Partial reloads: Every table interaction is an Inertia visit to the page the user is already on. List the props the table depends on in the
onlyprop and the server returns only those, leaving the rest of the page untouched — fewer queries per interaction and a much smaller response.
Sorting, filtering and pagination state live in the URL, so a filtered and sorted view stays intact across page reloads and can be shared or bookmarked. Use the name prop to prefix the query params when several tables live on the same page.
Getting Started
Already using Inertia Start?
Inertia Table is bundled with the starter kit: the components live in resources/js/components/inertia-table/, their TanStack feature configuration, types and helpers in resources/js/lib/inertia-table/, and spatie/laravel-query-builder is already required in composer.json. There is nothing to install, head straight to Usage.
Inertia Table is distributed as a shadcn-vue registry item: the CLI downloads the component from your account and writes the source files directly into your project, exactly like the shadcn components you already use.
Prerequisites
- A Vue 3 project already set up with shadcn-vue (a
components.jsonat its root) and Tailwind CSS - An active Inertia Table or Inertia Start license (an Inertia Start license includes Inertia Table)
Create an API Key
The registry is protected by an API key. Sign in to your account, open the Inertia Table product page, and create a key from the API keys section.
The key is displayed only once, so copy it right away. You can have up to 2 active keys at a time, and revoke them from the same section. Keys are shared across your products: a key created for Inertia Start works here too.
Add the Component
Run the shadcn-vue CLI from the root of your project, with your API key passed as the token query param:
npx shadcn-vue@latest add "https://app.inertiastart.com/registry/vue/inertia-table.json?token=YOUR_API_KEY"Keep the quotes around the URL: without them, your shell will cut it at the ?. This URL always resolves to the latest release; to pick an exact one, see Installing a specific version.
Treat the URL as a secret
The command embeds your API key. Avoid committing it to version control or pasting it into CI logs. In an automated environment, read it from an environment variable and revoke the key if it ever leaks.
Review What Was Installed
The CLI writes the source files into your project, following the aliases declared in your components.json:
| File | Default location |
|---|---|
InertiaTable.vue and its four sub-components | @/components/inertia-table/ |
features.ts, types.ts, utils.ts | @/lib/inertia-table/ |
It also installs the npm packages the component needs (@tanstack/vue-table, @inertiajs/vue3, @lucide/vue, @vueuse/core, clsx, reka-ui, tailwind-merge) and adds the shadcn-vue components it builds upon: badge, button, checkbox, command, context-menu, dialog, dropdown-menu, input, popover, select, separator and table.
From that point on the code is yours: the files are part of your codebase and can be customized freely.
Install the Backend Dependency
Server-side sorting and filtering rely on Spatie Query Builder, as shown in the backend example below:
composer require spatie/laravel-query-builderInstalling a specific version
The install command above always gives you the latest release. To install an exact version, add the version number to the registry path:
npx shadcn-vue@latest add "https://app.inertiastart.com/registry/vue/inertia-table/1.0.0.json?token=YOUR_API_KEY"Use the plain version number, without a leading v — 1.0.0, not v1.0.0. A version that was never published returns a 404.
Each release is stored as an immutable artifact, so a versioned URL always returns exactly the files that shipped with that version. Older releases stay available: as long as your license is active, you can install any version, not only the most recent one.
Find the version you need
The Releases section of the Inertia Table product page lists every version with its changelog. Each entry has a Copy install command button that hands you the command for that specific version, ready to paste.
Updating an existing installation
Updating is the same command run again: without a version to move to the latest release, or with one to pin to a chosen version. Downgrading works the same way — point the command at an older version.
The CLI rewrites the Inertia Table files in place, so commit your work before running it. Any change you made to InertiaTable.vue, its sub-components or the files in @/lib/inertia-table/ is overwritten, and the resulting diff is what shows you which customizations to re-apply.
When your license expires
The files already installed are part of your codebase and keep working. You will simply no longer be able to fetch the component from the registry, or receive new versions, until you renew your license.
Upgrading to Inertia Start
When you buy Inertia Table, you’ll receive 25% off Inertia Start.
Buy Inertia Table
Come back whenever you're ready
The discount does not expire. There is no upgrade window to miss.
Buy Inertia Start with the same account
Sign in with the account you used to buy Inertia Table, then open the Inertia Start purchase page. The discount is available only if you haven’t previously purchased Inertia Start. If eligible, it will be applied automatically at checkout—there’s nothing to claim, no coupon to request, and no code to enter.
Usage
Backend
Inertia Table integrates seamlessly with Spatie Query Builder to handle filtering and sorting on the server side efficiently.
Install Spatie Query Builder
Server-side sorting and filtering rely on the package, so it must be required in your Laravel application:
composer require spatie/laravel-query-builderRefer to the Spatie Query Builder documentation for the full list of available options.
Already using Inertia Start?
spatie/laravel-query-builder is already required in composer.json. There is nothing to install.
Declare what the table may filter and sort
In your controller, list the attributes exposed to the table with allowedFilters() and allowedSorts(), then return the paginated result, as shown in the example below.
Example
For our example, we will return a paginated result of users using Spatie Query Builder:
use App\Models\User;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Spatie\QueryBuilder\QueryBuilder;
public function index(Request $request)
{
$perPage = $request->integer('per_page', 10);
$perPage = in_array($perPage, [10, 25, 50, 100], true) ? $perPage : 10;
$users = QueryBuilder::for(User::class)
->allowedFilters('name', 'email')
->allowedSorts('email', 'name', 'created_at')
->paginate($perPage)
->withQueryString();
return Inertia::render('Users/Index', [
'users' => $users,
]);
}Inertia Table also supports API resources. So, you could also return a paginated resource collection:
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Spatie\QueryBuilder\QueryBuilder;
public function index(Request $request)
{
$perPage = $request->integer('per_page', 10);
$perPage = in_array($perPage, [10, 25, 50, 100], true) ? $perPage : 10;
$users = QueryBuilder::for(User::class)
->allowedFilters('name', 'email')
->allowedSorts('email', 'name', 'created_at')
->paginate($perPage)
->withQueryString();
return Inertia::render('Users/Index', [
'users' => UserResource::collection($users),
]);
}Frontend
InertiaTable.vue expects InertiaTableColumnDef definitions (using TanStack Table v9 column definitions), and a paginated response from Laravel.
- In Inertia mode, pass your paginated data to
dataand the component handles server-side pagination, sorting, and filters via query params (page,per_page,sort, andfilter[...]). - In API mode, omit
dataand provide aurl(optionally afetchCallback) so the component can fetch with Inertia'suseHttphook or your own fetcher.
Use search to render a search input (the input will be injected in a filter query param). When set to true, it writes to filter[search]; when set to a string, it writes to filter[<value>].
Use filters to define faceted filter dropdowns, and name to scope query params if multiple tables live on the same page (for example, users_sort, users_filter[status]).
Example
List users returned by our controller in our backend example using these settings:
- Display 3 columns:
email,name,created_at. - Display a search input that filters results by the
nameattribute (this requiresnameto be included inallowedFiltersin the controller’s query builder). - Allow sorting by
email,name,created_atcolumns (this requiresemail,name, andcreated_atto be included inallowedSortsin the controller’s query builder). - Allow hiding
namecolumn. - By default, display the most recent users first (sort by
created_atdescendant).
<script setup lang="ts">
import InertiaTable from '@/components/inertia-table/InertiaTable.vue';
import type { InertiaTableColumnDef } from '@/lib/inertia-table/features';
import type { InertiaTablePaginatedResource } from '@/lib/inertia-table/types';
interface User {
id: number;
name: string;
email: string;
created_at: string;
}
interface Props {
users: InertiaTablePaginatedResource<User>;
}
const props = defineProps<Props>();
const getColumns = (): InertiaTableColumnDef<User>[] => {
return [
{
accessorKey: 'email',
enableSorting: true,
header: () => 'Email',
cell: ({ row }) => row.getValue('email'),
},
{
accessorKey: 'name',
enableHiding: true,
enableSorting: true,
header: () => 'Name',
cell: ({ row }) => row.getValue('name'),
},
{
accessorKey: 'created_at',
enableSorting: true,
header: () => 'Created At',
cell: ({ row }) => new Date(row.getValue('created_at')).toLocaleString(),
},
];
};
</script>
<template>
<InertiaTable
:columns="getColumns()"
:data="props.users"
:default-sorting="[{ id: 'created_at', desc: true }]"
search="name"
/>
</template>TanStack feature configuration
TanStack Table v9 makes table features explicit and includes the selected feature set in its generic types. Inertia Table centralizes that configuration in @/lib/inertia-table/features and exports feature-aware aliases for columns, rows and table instances.
Use InertiaTableColumnDef<TData> for application column definitions instead of importing ColumnDef<TData> directly from @tanstack/vue-table. The alias supplies Inertia Table's sorting, column visibility and row selection features, so options such as enableSorting and enableHiding remain correctly typed.
TanStack table instance
InertiaTable component exposes the underlying table TanStack instance.
From the parent component, you can attach a ref to your InertiaTable component to access TanStack methods on the table instance, as shown in the following example:
<script setup lang="ts">
import { computed, ref } from 'vue';
import InertiaTable from '@/components/inertia-table/InertiaTable.vue';
const tableRef = ref<InstanceType<typeof InertiaTable> | null>(null);
const selectedRows = computed(() => {
return tableRef.value?.table.getSelectedRowModel().rows.map((row) => row.original) ?? [];
});
</script>
<template>
<InertiaTable ref="tableRef" :enable-row-selection="true" />
</template>Sorting
Sorting is enabled by default (enableSorting) and is opt-in per column: set enableSorting: true on the columns you want to make sortable in your InertiaTableColumnDef.
Sortable headers respond to two interactions:
- Left click cycles the column through ascending → descending → unsorted.
- Right click (long-press on touch devices) opens a context menu with explicit actions: Asc, Desc, Remove sorting (only when the column is currently sorted) and Hide (only when the column is hideable).
The state is serialized into a sort query param using the Spatie Query Builder convention, where a leading minus sign means descending — for example ?sort=-created_at. Changing the sorting resets the table to the first page.
Multi-column sorting
By default, sorting one column replaces the previous sorting. Set enableMultiSort to let users stack several columns instead:
<InertiaTable
:columns="getColumns()"
:data="props.users"
:enable-multi-sort="true"
/>Each sorted column is then appended to the sort query param in the order it was picked, comma-separated — for example ?sort=name,-created_at sorts by name ascending, then by creation date descending. Make sure every column involved is listed in allowedSorts in your controller's query builder.
Filters
To add filters, enable the feature using enableFiltering prop, and define your filters in the filters prop:
<InertiaTable
:columns="getColumns()"
:data="props.posts"
:enable-filtering="true"
:filters="[
{
columnId: 'category',
title: 'Categories',
options: [
{ value: 'cinema', label: 'Cinema' },
{ value: 'music', label: 'Music' },
{ value: 'tech', label: 'Tech' },
{ value: 'travel', label: 'Travel' },
],
},
]"
/>Row selection
You can enable row selection using the enableRowSelection prop on InertiaTable component:
<InertiaTable :enable-row-selection="true" ref="tableRef" />Then, you can retrieve the selected rows like this:
- Get all selected rows using
getSelectedRowModel():
// Get all selected rows (row objects)
const selectedRows = computed(() => {
return tableRef.value?.table.getSelectedRowModel().rows;
});
// Get the data of selected rows
const selectedItems = computed(() => {
return tableRef.value?.table.getSelectedRowModel().rows.map((row) => row.original) ?? [];
});- Get only the selected rows that are currently visible (after filtering), use
getFilteredSelectedRowModel():
const visibleSelectedRows = computed(() => {
return tableRef.value?.table.getFilteredSelectedRowModel().rows;
});For more information, refer to the TanStack Table Row Selection Guide.
Partial reloads
In Inertia mode, paginating, sorting, filtering, searching, changing the page size or resetting the table all trigger an Inertia visit to the page the user is already on. By default, the server re-renders that page and returns all of its props, even though only the paginated data actually changed.
The only prop turns those visits into partial reloads: pass the keys of the page props the table depends on, and the response contains only those.
<InertiaTable
:columns="getColumns()"
:data="props.users"
:only="['users']"
search="name"
/>The values must match the prop keys returned by Inertia::render() — users in our backend example — not column ids or the table name prop.
Lazy data evaluation
To get the most out of it, wrap the other props of the page in closures so Inertia evaluates them only when they are actually requested:
return Inertia::render('Users/Index', [
'users' => fn () => QueryBuilder::for(User::class)
->allowedFilters('name', 'email')
->allowedSorts('email', 'name', 'created_at')
->paginate($perPage)
->withQueryString(),
'teams' => fn () => Team::orderBy('name')->get(),
'stats' => fn () => User::selectRaw('...')->first(),
]);With :only="['users']", the teams and stats closures are never called on a table navigation. Without the closures, the queries still run on every keystroke in the search input, and only the payload gets smaller.
Benefits
- Fewer queries per interaction. The controller still runs, but Inertia resolves only the requested props. On a page where the table sits next to counters, charts or select options, that removes those queries from every sort, filter and page change.
- Smaller responses. The JSON carries one page of rows instead of the full prop set of the page. The difference grows with nested API resources, long option lists and slow connections, and it makes the table feel snappier because there is simply less to transfer and parse.
- Less client-side work. Props that are not returned keep their existing values and object identity, so Vue skips re-rendering the parts of the page bound to them and only the table updates.
- Independent tables. When several tables share a page, giving each one its own
onlymeans filtering one no longer re-runs the queries behind the others.
Excluded props go stale
Props left out of only are not refreshed on the client. If something else on the page has to stay in sync with the table — a total counter, a chart, filter options derived from the current query — list it too: :only="['users', 'stats']".
Props declared with Inertia::always() (such as errors) are still returned on a partial reload, and props declared with Inertia::optional() are returned only when listed in only. In API mode (url / fetchCallback), the component already requests the table endpoint alone, so only does not apply.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
name? | string | undefined | Prefix query params to avoid collisions when multiple tables are present on the same page. |
data? | InertiaTablePaginatedData<TData> | InertiaTablePaginatedResource<TData> | null | undefined | Paginated data from Laravel. Required for Inertia mode. |
url? | string | undefined | Base URL to fetch from in API mode (used when data is not provided). |
fetchCallback? | (url: string) => Promise<InertiaTablePaginatedData<TData> | InertiaTablePaginatedResource<TData>> | undefined | Custom fetcher for API mode. Defaults to Inertia's useHttp hook. |
columns | InertiaTableColumnDef<TData>[] | undefined | Feature-aware column definitions for Inertia Table's TanStack configuration. |
pageSizes? | number[] | [10, 25, 50, 100] | Page-size options for the paginator. |
only? | string[] | undefined | Page prop keys to request on table navigation, turning each visit into a partial reload. Inertia mode only. |
preserveState? | boolean | true | Preserve Inertia page state on navigation. |
preserveScroll? | boolean | true | Preserve scroll position on navigation. |
enableColumnToggle? | boolean | true | Show column visibility toggle. |
enableFiltering? | boolean | true | Show search and faceted filters. |
enableSorting? | boolean | true | Enable server-side sorting (sort query param). |
enableMultiSort? | boolean | false | Allow sorting on several columns at once (comma-separated sort query param). |
enableRowSelection? | boolean | false | Show row selection checkboxes. |
defaultSorting? | SortingState | [] | Initial sorting before reading URL params. |
search? | true | string | undefined | Render search input; true uses filter[search], string uses filter[<value>] query param. |
filters? | InertiaTableFilter[] | undefined | Faceted filter definitions (filter[<columnId>] query param). |
labels? | InertiaTableLabels | search: "Search", reset: "Reset", selectAll: "Select all", selectRow: "Select row", noResults: "No results found." | UI text overrides. |
columnHeaderLabels? | InertiaTableColumnHeaderLabels | ascDirection: "Asc", descDirection: "Desc", hide: "Hide", removeSorting: "Remove sorting" | Text overrides for the column header context menu. |
Types and interfaces
features.ts registers the TanStack Table features used by Inertia Table and exports the matching generic aliases:
import type { Column, ColumnDef, HeaderContext, Row, RowData, Table } from '@tanstack/vue-table';
import {
columnVisibilityFeature,
rowSelectionFeature,
rowSortingFeature,
tableFeatures,
} from '@tanstack/vue-table';
export const inertiaTableFeatures = tableFeatures({
columnVisibilityFeature,
rowSelectionFeature,
rowSortingFeature,
});
export type InertiaTableFeatures = typeof inertiaTableFeatures;
export type InertiaTableColumnDef<TData extends RowData> = ColumnDef<InertiaTableFeatures, TData>;
export type InertiaTableColumn<TData extends RowData, TValue = unknown> = Column<
InertiaTableFeatures,
TData,
TValue
>;
export type InertiaTableHeaderContext<TData extends RowData, TValue = unknown> = HeaderContext<
InertiaTableFeatures,
TData,
TValue
>;
export type InertiaTableRow<TData extends RowData> = Row<InertiaTableFeatures, TData>;
export type InertiaTableInstance<TData extends RowData> = Table<InertiaTableFeatures, TData>;The pagination and filter types live in types.ts:
import type { Component } from 'vue';
export interface InertiaTablePaginatedData<T> {
data: T[];
current_page: number;
from: number;
to: number;
last_page: number;
links: Array<{
url: string | null;
label: string;
active: boolean;
page: number | null;
}>;
prev_page_url: string | null;
next_page_url: string | null;
first_page_url: string;
last_page_url: string;
path: string;
per_page: number;
total: number;
}
export interface InertiaTablePaginatedResource<T> {
data: T[];
links: {
first: string;
last: string;
next: string | null;
prev: string | null;
};
meta: {
current_page: number;
from: number;
to: number;
last_page: number;
links: Array<{
url: string | null;
label: string;
active: boolean;
page: number | null;
}>;
path: string;
per_page: number;
total: number;
};
}
export interface InertiaTableFilterOption {
value: string;
label: string;
icon?: Component;
}
export interface InertiaTableFilter {
columnId: string;
title: string;
icon?: Component;
options: InertiaTableFilterOption[];
}Advanced usage and guides
For advanced usage or to get more information on all the capabilities of TanStack Table, refer to their documentation.