Describe it once
A table is described in PHP: the query, the columns, the filters, the actions. It reaches the page as an ordinary Inertia prop, and one component renders it.
return Inertia::render('Projects/Index', [
'projects' => Table::make('projects')
->query(Project::query()->where('team_id', $request->user()->team_id))
->columns([
TextColumn::make('name')->searchable()->sortable(),
TextColumn::make('status')->badge(),
TextColumn::make('budget')->money('USD')->sortable(),
])
->filters([
SelectFilter::make('status')->options([
'active' => 'Active', 'completed' => 'Completed',
]),
])
->savedViews('team:'.$request->user()->team_id),
]);
<script lang="ts">
import { Table } from '@spindle/table-svelte';
import type { TablePayload } from '@spindle/table-core';
let { projects }: { projects: TablePayload } = $props();
</script>
<Table table={projects} prop="projects" />
That is the whole integration. The getting-started guide has the full controller.
No table routes
There is no table API to build, secure or keep in sync. Sorting, filtering and paging are ordinary Inertia partial reloads of the page you are already on. Actions, inline edits and saved views post back to that same page, run through its own middleware and policies, and the page answers with fresh props in the same response.
Inline edits are optimistic and only the edited row comes back. Validation errors land in the normal Inertia error bag.
Fast, even with thousands of rows
Rows are virtualized, so 5,000 loaded rows cost about the same as 100. The DOM holds roughly what is on screen.
| Desktop (Apple M4) | Before | Now |
|---|---|---|
| First render, 1,000 rows | 986 ms | 77 ms |
| First render, 5,000 rows | 4,008 ms | 85 ms |
| Select all, 5,000 rows | 1,157 ms | 13 ms |
| DOM elements, 5,000 rows | 360,137 | 2,459 |
Measured side by side with Filament on twin tables, interactions like sorting, paging and search are 25–50% faster, and up to twice as fast with 100 rows per page or a slower CPU. Responses are 5–7× smaller, because only the table data travels, as JSON. The benchmarks have every number, the setup and the caveats, including where Filament wins.
Feels like an app

- A sticky, translucent header, hairline rows, segmented views and filter chips.
- Keyboard first: a filter menu on
for/, and a grid with a single Tab stop and spreadsheet-style arrow keys. - Drag or Shift to select ranges, with a floating bar for bulk actions.
- A spreadsheet-style grid: double-click, Enter or just type to edit a cell, select ranges, and copy as TSV or HTML.
- Drag a header to group, with per-group totals and a drag-and-drop group panel.
- Responsive cards and collapsible details on small screens.
- Every color, size and motion value is a
--st-*CSS variable, and every part has a class, adata-slotand an override. - Accessible by default: 3:1 control contrast, forced-colors support and screen reader announcements.
Built for real work
- Columns: text, badges, money and dates, sanitized HTML and Markdown, syntax-highlighted code, images, icons, colors, and editable cells.
- Filters: select, range, date and custom filters, remote option search, and a nested AND/OR rule builder.
- Actions: toolbar, row, cell and bulk actions, policy-backed CRUD helpers, and confirmations. The PHP side authorizes and executes; the UI is yours.
- Saved views: preset tabs, private views, defaults and favorites.
- Imports and exports: streamed CSV export, queued CSV and XLSX exports, and queued CSV imports with private failure reports.
- Background jobs: bulk work on queued workers, with progress, cancellation and history.
Headless at the core
| Package | Purpose |
|---|---|
spindle/table |
Query, schema, authorization, mutation and saved-view layer |
@spindle/table-core |
Headless state, selection, views, formatting and Inertia navigation. Zero runtime dependencies. |
@spindle/table-svelte |
Accessible prebuilt components on shadcn-svelte, bits-ui and Tailwind CSS v4 |
The core does not know what renders it. Build a completely custom UI on it, or a new adapter on the wire protocol. Vue and React adapters are on the list.
Coming from Filament?
The API is inspired by Filament, so it should feel familiar. It is not a drop-in adapter, and full feature parity is still a work in progress. Coming from Filament shows what maps across and what is left.
Status
In development, and not yet on Packagist or npm. It needs PHP 8.3+, Laravel 12 or 13, Inertia 3.4+ and Svelte 5. Start with getting started.