inertia start
Components

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.

If you don't need the full starter kit, you can also buy Inertia Table separately.

See Inertia Table in action

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 sort query param, so it applies to the whole dataset and not only the current page. Enable it per column with enableSorting in your ColumnDef, set an initial order with the defaultSorting prop, and allow sorting on several columns at once with enableMultiSort.
  • Filtering: Two complementary ways to narrow results, both sent server-side as filter[...] query params. The search prop renders a text input for free-text search, and the filters prop renders faceted dropdowns with predefined options. Each filtered attribute must be declared in allowedFilters on 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 page and per_page query params. Customize the available page sizes with the pageSizes prop.
  • Visibility: A dropdown lets users show or hide columns, so dense tables stay readable on smaller screens. Mark a column as hideable with enableHiding in its ColumnDef; the toggle itself is controlled by the enableColumnToggle prop.
  • Selectable: Set enableRowSelection to add checkboxes to each row plus a "select all" checkbox in the header. Access the selected rows from the parent component through the exposed TanStack table instance (getSelectedRowModel()) to build your own bulk actions such as deleting or exporting.

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.

Usage

Backend

Inertia Table integrates seamlessly with Spatie Query Builder to handle filtering and sorting on the server side efficiently.

Example

For our example, we will return a paginated result of users using Spatie Query Builder:

app/Http/Controllers/UserController.php
use App\Models\User;
use Illuminate\Http\Request;
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:

app/Http/Controllers/UserController.php
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\Request;
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 ColumnDef definitions and a paginated response from Laravel.

  • In Inertia mode, pass your paginated data to data and the component handles server-side pagination, sorting, and filters via query params (page, per_page, sort, and filter[...]).
  • In API mode, omit data and provide a url (optionally a fetchCallback) so the component can fetch with axios 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 name attribute (this requires name to be included in allowedFilters in the controller’s query builder).
  • Allow sorting by email, name, created_at columns (this requires email, name, and created_at to be included in allowedSorts in the controller’s query builder).
  • Allow hiding name column.
  • By default, display the most recent users first (sort by created_at descendant).
<script setup lang="ts">
import type { ColumnDef } from '@tanstack/vue-table';
import { h } from 'vue';
import InertiaTable from '@/components/inertia-table/InertiaTable.vue';
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 = (): ColumnDef<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 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 ColumnDef.

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.

Props

PropTypeDefaultDescription
name?stringundefinedPrefix query params to avoid collisions when multiple tables are present on the same page.
data?InertiaTablePaginatedData<TData> | InertiaTablePaginatedResource<TData> | nullundefinedPaginated data from Laravel. Required for Inertia mode.
url?stringundefinedBase URL to fetch from in API mode (used when data is not provided).
fetchCallback?(url: string) => Promise<InertiaTablePaginatedData<TData> | InertiaTablePaginatedResource<TData>>undefinedCustom fetcher for API mode. Defaults to axios.
columnsColumnDef<TData>[]undefinedColumn definitions (TanStack Table).
pageSizes?number[][10, 25, 50, 100]Page-size options for the paginator.
preserveState?booleantruePreserve Inertia page state on navigation.
preserveScroll?booleantruePreserve scroll position on navigation.
enableColumnToggle?booleantrueShow column visibility toggle.
enableFiltering?booleantrueShow search and faceted filters.
enableSorting?booleantrueEnable server-side sorting (sort query param).
enableMultiSort?booleanfalseAllow sorting on several columns at once (comma-separated sort query param).
enableRowSelection?booleanfalseShow row selection checkboxes.
defaultSorting?SortingState[]Initial sorting before reading URL params.
search?true | stringundefinedRender search input; true uses filter[search], string uses filter[<value>] query param.
filters?InertiaTableFilter[]undefinedFaceted filter definitions (filter[<columnId>] query param).
labels?InertiaTableLabelssearch: "Search", reset: "Reset", selectAll: "Select all", selectRow: "Select row", noResults: "No results found."UI text overrides.
columnHeaderLabels?InertiaTableColumnHeaderLabelsascDirection: "Asc", descDirection: "Desc", hide: "Hide", removeSorting: "Remove sorting"Text overrides for the column header context menu.

Types and interfaces

@/lib/inertia-table/types
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.

On this page