Migrating from svelte-headless-table

svelte-headless-table popularised the headless-table pattern in the Svelte ecosystem. It is built on Svelte 4 stores and plugins (addSortBy, addColumnFilters, addPagination, ...), and it leaves the markup to you. SvGrid keeps the same headless idea but runs on Svelte 5 runes and ships a render component, so the port mostly removes the table you used to hand-author.

Estimated effort: 1-3 hours per grid. Most of the time is deleting the <table> markup and Subscribe blocks you no longer need.

Know your options first

The last svelte-headless-table release was 0.18.3 in October 2024 and it declares svelte@^4, so a Svelte 5 upgrade forces a decision. There are three honest answers and you should know all of them:

  1. @humanspeak/svelte-headless-table - a maintained fork on Svelte 5 with the same API. Changing one package name is the cheapest path by a wide margin. If your table works and you only need Svelte 5, do that.
  2. TanStack Table v9 - shipped a Svelte 5 adapter in August 2026. Still headless-only, so you keep writing and maintaining the markup.
  3. SvGrid - this page. A different trade: you delete the markup and take a renderer instead.

Pick SvGrid when the markup is the part you are tired of. Note that the blocker is the peer range, not the syntax: [email protected] declares svelte@^4, so installing it beside Svelte 5 is a peer conflict. Slots and let: themselves still work in Svelte 5 - they are deprecated in favour of snippets, not removed - so the Subscribe blocks below keep rendering. What you cannot do is pass slotted content to a component that renders with {@render ...}, which is why the pattern grates in a runes codebase.

Run the codemod

The example at the end of this page runs against these rows:

<script lang="ts">
  import { SvGrid, type GridColumns } from '@svgrid/grid'

  type Person = {
    id: number
    name: string
    department: string
    city: string
    age: number
    salary: number
  }

  const people: Person[] = [
    { id: 1, name: 'Ada Lovelace',   department: 'Engineering', city: 'London',   age: 36, salary: 142000 },
    { id: 2, name: 'Grace Hopper',   department: 'Engineering', city: 'New York', age: 45, salary: 168000 },
    { id: 3, name: 'Linus Torvalds', department: 'Platform',    city: 'Portland', age: 54, salary: 155000 },
    { id: 4, name: 'Radia Perlman',  department: 'Networking',  city: 'Seattle',  age: 49, salary: 161000 },
    { id: 5, name: 'Barbara Liskov', department: 'Platform',    city: 'Boston',   age: 52, salary: 172000 },
  ]

  let rows = $state<Person[]>(people)

  const columns: GridColumns<Person> = [
    { field: 'name',       header: 'Name',       width: 200, editorType: 'text' },
    { field: 'department', header: 'Department', width: 150, editorType: 'text' },
    { field: 'city',       header: 'City',       width: 140, editorType: 'text' },
    { field: 'age',        header: 'Age',        width: 90,  editorType: 'number' },
    { field: 'salary',     header: 'Salary',     width: 130, editorType: 'number', format: { type: 'currency', currency: 'USD' } },
  ]
</script>
npx @svgrid/migrate             # preview the result
npx @svgrid/migrate src --write # apply it

It translates column definitions and plugin config, deletes the Subscribe/Render scaffolding, and reports anything it cannot map rather than dropping it silently. It previews by default. See @svgrid/migrate for the full mapping and its limits.

Vocabulary cheat sheet

svelte-headless-table sv-grid
createTable(data, plugins) createSvGrid({...}) or <SvGrid>
table.createColumns((t) => [...]) columns: ColumnDef[]
t.column({ accessor: 'x', header }) { field: 'x', header }
t.column({ accessor: (r) => ... }) { id, fieldFn: (r) => ... }
t.group({ header, columns }) { header, columns: [...] } (column group)
addSortBy() rowSortingFeature
addColumnFilters() / addTableFilter() columnFilteringFeature
addPagination() Built in; toggle showPagination
addExpandedRows() / addSubRows() rowExpandingFeature
addGroupBy() columnGroupingFeature + api.setGroupBy()
addSelectedRows() rowSelectionFeature
addDataExport() @svgrid/enterprise export pack
createViewModel(columns) + Subscribe <SvGrid> (no view model to wire)
pluginStates.sort.sortKeys api.setSort(id, dir) / onSortingChange

The shortcut form

That table maps plugins onto the headless features, which is what you want if you keep driving the engine yourself. If you are moving to the <SvGrid> component, every capability also has a boolean prop, and that is what the codemod emits:

Plugin <SvGrid> prop
addSortBy() sortable
addTableFilter() filterable showGlobalFilter
addColumnFilters() filterable showColumnFilters
addPagination({ initialPageSize: 25 }) pageable pageSize={25}
addSelectedRows() showRowSelection
addGroupBy() groupable
addSubRows() treeData
addColumnOrder() enableColumnReorder
addResizedColumns() nothing - resizing is built in
addHiddenColumns() visible: false on the column
addExpandedRows() treeData, or isDetailRow + renderDetailRow
addGridLayout() nothing - SvGrid owns its layout

Every capability is off by default and turned on by its prop; there is no plugin registration step.

Before / after

- <script>
-   import { createTable } from 'svelte-headless-table'
-   import { addSortBy, addColumnFilters, addPagination } from 'svelte-headless-table/plugins'
-   import { readable } from 'svelte/store'
-
-   const table = createTable(readable(data), {
-     sort: addSortBy(), filter: addColumnFilters(), page: addPagination(),
-   })
-   const columns = table.createColumns((t) => [
-     t.column({ accessor: 'name',   header: 'Name' }),
-     t.column({ accessor: 'amount', header: 'Amount' }),
-   ])
-   const { headerRows, rows, tableAttrs, tableBodyAttrs } = table.createViewModel(columns)
- </script>
-
- <table {...$tableAttrs}>
-   <thead> ...Subscribe over headerRows... </thead>
-   <tbody {...$tableBodyAttrs}> ...Subscribe over rows... </tbody>
- </table>

+ <script lang="ts">
+   import {
+     SvGrid, tableFeatures, rowSortingFeature, columnFilteringFeature,
+     type ColumnDef,
+   } from '@svgrid/grid'
+
+   const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })
+   const columns: ColumnDef<typeof features, Row>[] = [
+     { field: 'name',   header: 'Name' },
+     { field: 'amount', header: 'Amount', format: { type: 'currency', currency: 'USD' } },
+   ]
+ </script>
+
+ <SvGrid data={rows} columns={columns} features={features} showPagination />

Type the column array against your row type. GridColumns<(typeof data)[number]> is the shortest form; a bare GridColumns widens the row to Record<string, unknown> and stops checking field against your real keys.

What you get for free

What changes

What does not come across

When not to migrate

Frequently asked questions

How hard is it to move from svelte-headless-table to SvGrid?

Usually 1-3 hours per grid, and npx @svgrid/migrate does the mechanical part. The plugin-to-feature mapping is almost one-to-one; most of the work is deleting the createViewModel + Subscribe + <table> markup, because SvGrid renders that for you.

Does SvGrid use Svelte 5 runes instead of stores?

Yes. svelte-headless-table is built on Svelte 4 stores; SvGrid is Svelte-5 native ($state / $derived / $effect) with snippets for custom cells.

Is SvGrid still headless like svelte-headless-table?

Yes - createSvGrid plus the row-model factories is a headless engine you can drive with your own markup, importable on its own from @svgrid/grid/core. The difference is that SvGrid also ships a batteries-included <SvGrid> component so you usually do not have to.

Is svelte-headless-table still maintained?

The original has not published since 0.18.3 in October 2024 and targets Svelte 4. A community fork, @humanspeak/svelte-headless-table, is maintained and runs on Svelte 5 with the same API, which is the lowest-effort option if you are happy with the library and only need Svelte 5.

What you end up with

Sorting, filtering and selection, on a library that still ships for Svelte 5.

<SvGrid data={rows} {columns} sortable filterable selectable />

See also

Related articles