v9.0.0-beta.10 introduces a breaking change in how row models are defined in order to bring increased type-safety features. Row model factories and function registries now live as slots on the features object instead of a separate rowModels option, and the factories no longer take arguments. If you migrated on an earlier beta, see the Row Models section below for the new shape.
TanStack Table v9 is a major release with explicit feature registration, row model registration, and a new atom-backed state model. The Svelte adapter was also rewritten around Svelte 5 runes.
The main changes are the Svelte 5 requirement, the new createTable entrypoint, explicit features (including row models registered as feature slots), and the move from v8 writable-store patterns to v9 runes and atoms.
The v9 Svelte adapter only supports Svelte 5+. It is built on Svelte 5 runes such as $state, $derived.by, and $effect.pre.
If your app is still on Svelte 3 or Svelte 4, choose one of these paths:
There is no Svelte 3/4 compatibility shim for the v9 Svelte adapter.
// v8
import { createSvelteTable } from '@tanstack/svelte-table'
const table = createSvelteTable(options)
// v9
import { createTable } from '@tanstack/svelte-table'
const table = createTable(options)// v8
import { createSvelteTable } from '@tanstack/svelte-table'
const table = createSvelteTable(options)
// v9
import { createTable } from '@tanstack/svelte-table'
const table = createTable(options)// v8
import {
createSvelteTable,
getCoreRowModel,
} from '@tanstack/svelte-table'
const table = createSvelteTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
})
// v9
import { createTable, tableFeatures } from '@tanstack/svelte-table'
const features = tableFeatures({})
const table = createTable({
features,
columns,
get data() {
return data
},
})// v8
import {
createSvelteTable,
getCoreRowModel,
} from '@tanstack/svelte-table'
const table = createSvelteTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
})
// v9
import { createTable, tableFeatures } from '@tanstack/svelte-table'
const features = tableFeatures({})
const table = createTable({
features,
columns,
get data() {
return data
},
})In Svelte 5, pass reactive values like data through getters so table options read the current rune value.
Features control which APIs, options, and state slices exist on the table.
import {
columnFilteringFeature,
columnVisibilityFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
tableFeatures,
} from '@tanstack/svelte-table'
const features = tableFeatures({
columnFilteringFeature,
columnVisibilityFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
})import {
columnFilteringFeature,
columnVisibilityFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
tableFeatures,
} from '@tanstack/svelte-table'
const features = tableFeatures({
columnFilteringFeature,
columnVisibilityFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
})If a feature is not registered, its APIs and state slice are not available.
stockFeatures is useful for early migration when you have not audited feature usage yet.
import { createTable, stockFeatures } from '@tanstack/svelte-table'
const table = createTable({
features: stockFeatures,
columns,
get data() {
return data
},
})import { createTable, stockFeatures } from '@tanstack/svelte-table'
const table = createTable({
features: stockFeatures,
columns,
get data() {
return data
},
})Use it as a temporary migration shortcut. Explicit feature registration is the production target.
| Feature | Import Name |
|---|---|
| Column Filtering | columnFilteringFeature |
| Global Filtering | globalFilteringFeature |
| Row Sorting | rowSortingFeature |
| Row Pagination | rowPaginationFeature |
| Row Selection | rowSelectionFeature |
| Row Expanding | rowExpandingFeature |
| Row Pinning | rowPinningFeature |
| Column Pinning | columnPinningFeature |
| Column Visibility | columnVisibilityFeature |
| Column Ordering | columnOrderingFeature |
| Column Sizing | columnSizingFeature |
| Column Resizing | columnResizingFeature |
| Column Grouping | columnGroupingFeature |
| Column Faceting | columnFacetingFeature |
Row model factories now live as slots directly inside tableFeatures({...}). The rowModels option no longer exists.
| v8 Option | v9 tableFeatures Slot | v9 Factory Function |
|---|---|---|
| getCoreRowModel() | (automatic) | Not needed |
| getFilteredRowModel() | filteredRowModel | createFilteredRowModel() |
| getSortedRowModel() | sortedRowModel | createSortedRowModel() |
| getPaginationRowModel() | paginatedRowModel | createPaginatedRowModel() |
| getExpandedRowModel() | expandedRowModel | createExpandedRowModel() |
| getGroupedRowModel() | groupedRowModel | createGroupedRowModel() |
| getFacetedRowModel() | facetedRowModel | createFacetedRowModel() |
| getFacetedMinMaxValues() | facetedMinMaxValues | createFacetedMinMaxValues() |
| getFacetedUniqueValues() | facetedUniqueValues | createFacetedUniqueValues() |
Function registries move to slots too: pass filterFns, sortFns, and aggregationFns directly to tableFeatures instead of as factory arguments.
<script lang="ts">
// v8
import {
createSvelteTable,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
filterFns,
sortingFns,
} from '@tanstack/svelte-table'
const table = createSvelteTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
filterFns,
sortingFns,
})
</script><script lang="ts">
// v8
import {
createSvelteTable,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
filterFns,
sortingFns,
} from '@tanstack/svelte-table'
const table = createSvelteTable({
columns,
data,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
filterFns,
sortingFns,
})
</script><script lang="ts">
// v9
import {
columnFilteringFeature,
createFilteredRowModel,
createPaginatedRowModel,
createSortedRowModel,
createTable,
filterFns,
rowPaginationFeature,
rowSortingFeature,
sortFns,
tableFeatures,
} from '@tanstack/svelte-table'
const features = tableFeatures({
columnFilteringFeature,
rowPaginationFeature,
rowSortingFeature,
filteredRowModel: createFilteredRowModel(),
sortedRowModel: createSortedRowModel(),
paginatedRowModel: createPaginatedRowModel(),
filterFns,
sortFns,
})
let data = $state(makeData(1000))
const table = createTable({
features,
columns,
get data() {
return data
},
})
</script><script lang="ts">
// v9
import {
columnFilteringFeature,
createFilteredRowModel,
createPaginatedRowModel,
createSortedRowModel,
createTable,
filterFns,
rowPaginationFeature,
rowSortingFeature,
sortFns,
tableFeatures,
} from '@tanstack/svelte-table'
const features = tableFeatures({
columnFilteringFeature,
rowPaginationFeature,
rowSortingFeature,
filteredRowModel: createFilteredRowModel(),
sortedRowModel: createSortedRowModel(),
paginatedRowModel: createPaginatedRowModel(),
filterFns,
sortFns,
})
let data = $state(makeData(1000))
const table = createTable({
features,
columns,
get data() {
return data
},
})
</script>Svelte v9 table state is atom-backed and rune-aware. Do not port v8 writable-store table option patterns directly except as "before" code.
| Surface | Use |
|---|---|
| table.state | Full registered table state by default, or selected state from the second argument to createTable. |
| table.store.state | Current full table state snapshot. |
| table.atoms.<slice>.get() | Narrow current-value read for one state slice. |
| subscribeTable(source, selector?) | Fine-grained Svelte subscription helper that exposes .current. |
| table.baseAtoms.<slice> | Internal writable atoms. Prefer feature APIs or external atoms. |
// v8
const sorting = table.getState().sorting
// v9: full snapshot
const sorting = table.store.state.sorting
// v9: narrow atom read
const sorting = table.atoms.sorting.get()// v8
const sorting = table.getState().sorting
// v9: full snapshot
const sorting = table.store.state.sorting
// v9: narrow atom read
const sorting = table.atoms.sorting.get()By default, table.state contains the full registered table state.
const table = createTable({
features,
columns,
get data() {
return data
},
})
const { pagination, sorting } = table.stateconst table = createTable({
features,
columns,
get data() {
return data
},
})
const { pagination, sorting } = table.statePass a second-argument selector when you want table.state to contain only the values that should cause reactive updates.
const table = createTable(
{
features,
columns,
get data() {
return data
},
},
(state) => ({
pagination: state.pagination,
}),
)
table.state.paginationconst table = createTable(
{
features,
columns,
get data() {
return data
},
},
(state) => ({
pagination: state.pagination,
}),
)
table.state.paginationPassing (state) => state is equivalent to the default selector and is no longer necessary.
import { subscribeTable } from '@tanstack/svelte-table'
const pagination = subscribeTable(table.atoms.pagination)
const pageIndex = subscribeTable(
table.atoms.pagination,
(pagination) => pagination.pageIndex,
)import { subscribeTable } from '@tanstack/svelte-table'
const pagination = subscribeTable(table.atoms.pagination)
const pageIndex = subscribeTable(
table.atoms.pagination,
(pagination) => pagination.pageIndex,
)<strong>
Page {pagination.current.pageIndex + 1} of {table.getPageCount()}
</strong><strong>
Page {pagination.current.pageIndex + 1} of {table.getPageCount()}
</strong>The v8-style state plus on[State]Change pattern still works in v9 and is the most direct migration path, but External Atoms (below) are the preferred v9 way to own state slices outside the table.
Use createTableState for Svelte-owned state slices that need to accept TanStack Table updater functions.
<script lang="ts">
import {
createTable,
createTableState,
type PaginationState,
type SortingState,
} from '@tanstack/svelte-table'
let data = $state(makeData(1000))
const [sorting, setSorting] = createTableState<SortingState>([])
const [pagination, setPagination] = createTableState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const table = createTable({
features,
columns,
get data() {
return data
},
state: {
get sorting() {
return sorting()
},
get pagination() {
return pagination()
},
},
onSortingChange: setSorting,
onPaginationChange: setPagination,
})
</script><script lang="ts">
import {
createTable,
createTableState,
type PaginationState,
type SortingState,
} from '@tanstack/svelte-table'
let data = $state(makeData(1000))
const [sorting, setSorting] = createTableState<SortingState>([])
const [pagination, setPagination] = createTableState<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const table = createTable({
features,
columns,
get data() {
return data
},
state: {
get sorting() {
return sorting()
},
get pagination() {
return pagination()
},
},
onSortingChange: setSorting,
onPaginationChange: setPagination,
})
</script>The v8-style onStateChange callback is gone. Use per-slice on[State]Change callbacks or external atoms.
Use external atoms when the app should own and share state slices outside the table.
<script lang="ts">
import { createAtom, useSelector } from '@tanstack/svelte-store'
import type {
PaginationState,
SortingState,
} from '@tanstack/svelte-table'
const sortingAtom = createAtom<SortingState>([])
const paginationAtom = createAtom<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const sorting = useSelector(sortingAtom)
const pagination = useSelector(paginationAtom)
const table = createTable({
features,
columns,
get data() {
return data
},
atoms: {
sorting: sortingAtom,
pagination: paginationAtom,
},
})
</script>
<span>Page {pagination.current.pageIndex + 1}</span><script lang="ts">
import { createAtom, useSelector } from '@tanstack/svelte-store'
import type {
PaginationState,
SortingState,
} from '@tanstack/svelte-table'
const sortingAtom = createAtom<SortingState>([])
const paginationAtom = createAtom<PaginationState>({
pageIndex: 0,
pageSize: 10,
})
const sorting = useSelector(sortingAtom)
const pagination = useSelector(paginationAtom)
const table = createTable({
features,
columns,
get data() {
return data
},
atoms: {
sorting: sortingAtom,
pagination: paginationAtom,
},
})
</script>
<span>Page {pagination.current.pageIndex + 1}</span>Do not provide both atoms.pagination and state.pagination; the atom owns that slice.
Column helpers and column types now include TFeatures first.
// v8
const columnHelper = createColumnHelper<Person>()
const columns: ColumnDef<Person>[] = [
columnHelper.accessor('age', {
header: 'Age',
sortingFn: 'alphanumeric',
}),
]
// v9
const columnHelper = createColumnHelper<typeof features, Person>()
const columns: Array<ColumnDef<typeof features, Person>> = columnHelper.columns([
columnHelper.accessor('age', {
header: 'Age',
sortFn: 'alphanumeric',
}),
])// v8
const columnHelper = createColumnHelper<Person>()
const columns: ColumnDef<Person>[] = [
columnHelper.accessor('age', {
header: 'Age',
sortingFn: 'alphanumeric',
}),
]
// v9
const columnHelper = createColumnHelper<typeof features, Person>()
const columns: Array<ColumnDef<typeof features, Person>> = columnHelper.columns([
columnHelper.accessor('age', {
header: 'Age',
sortFn: 'alphanumeric',
}),
])Use columnHelper.columns([...]) for better inference across nested columns.
Replace v8 flexRender calls with the Svelte FlexRender component.
<!-- v8 -->
<svelte:component
this={flexRender(header.column.columnDef.header, header.getContext())}
/>
<!-- v9 -->
<FlexRender {header} />
<FlexRender {cell} /><!-- v8 -->
<svelte:component
this={flexRender(header.column.columnDef.header, header.getContext())}
/>
<!-- v9 -->
<FlexRender {header} />
<FlexRender {cell} />For Svelte components in column definitions, use renderComponent.
import { renderComponent } from '@tanstack/svelte-table'
import StatusCell from './StatusCell.svelte'
const columns = columnHelper.columns([
columnHelper.accessor('status', {
cell: ({ row }) => renderComponent(StatusCell, { row }),
}),
])import { renderComponent } from '@tanstack/svelte-table'
import StatusCell from './StatusCell.svelte'
const columns = columnHelper.columns([
columnHelper.accessor('status', {
cell: ({ row }) => renderComponent(StatusCell, { row }),
}),
])For Svelte snippets, use renderSnippet.
<script lang="ts">
import { renderSnippet } from '@tanstack/svelte-table'
const columns = columnHelper.columns([
columnHelper.accessor('firstName', {
cell: ({ row }) => renderSnippet(nameCell, row),
}),
])
</script>
{#snippet nameCell(row)}
<strong>{row.original.firstName}</strong>
{/snippet}<script lang="ts">
import { renderSnippet } from '@tanstack/svelte-table'
const columns = columnHelper.columns([
columnHelper.accessor('firstName', {
cell: ({ row }) => renderSnippet(nameCell, row),
}),
])
</script>
{#snippet nameCell(row)}
<strong>{row.original.firstName}</strong>
{/snippet}tableOptions() helps compose shared table option fragments.
import { tableOptions } from '@tanstack/svelte-table'
const baseOptions = tableOptions({
features,
defaultColumn: {
minSize: 40,
},
})
const table = createTable({
...baseOptions,
columns,
get data() {
return data
},
})import { tableOptions } from '@tanstack/svelte-table'
const baseOptions = tableOptions({
features,
defaultColumn: {
minSize: 40,
},
})
const table = createTable({
...baseOptions,
columns,
get data() {
return data
},
})createTableHook creates shared Svelte table helpers with features (including row model slots) and registered components already bound.
import { createTableHook } from '@tanstack/svelte-table'
export const { createAppTable, createAppColumnHelper } = createTableHook({ features })
const columnHelper = createAppColumnHelper<Person>()
const table = createAppTable({
columns,
get data() {
return data
},
})import { createTableHook } from '@tanstack/svelte-table'
export const { createAppTable, createAppColumnHelper } = createTableHook({ features })
const columnHelper = createAppColumnHelper<Person>()
const table = createAppTable({
columns,
get data() {
return data
},
})See the Composable Tables Guide for full patterns.
Table-level enablePinning split into:
enableColumnPinning: true
enableRowPinning: trueenableColumnPinning: true
enableRowPinning: trueColumn resizing now has its own feature and state slice.
const features = tableFeatures({
columnSizingFeature,
columnResizingFeature,
})const features = tableFeatures({
columnSizingFeature,
columnResizingFeature,
})columnSizingInfo became columnResizing, and onColumnSizingInfoChange became onColumnResizingChange.
| v8 | v9 |
|---|---|
| sortingFn | sortFn |
| sortingFns | sortFns |
| getSortingFn() | getSortFn() |
| getAutoSortingFn() | getAutoSortFn() |
| SortingFn | SortFn |
Underscore-prefixed APIs that are now public should be called without _, such as row.getAllCellsByColumnId().
Use TFeatures as the first generic:
ColumnDef<typeof features, Person>
Column<typeof features, Person>
Row<typeof features, Person>
Table<typeof features, Person>ColumnDef<typeof features, Person>
Column<typeof features, Person>
Row<typeof features, Person>
Table<typeof features, Person>const features = tableFeatures({
rowSortingFeature,
rowPaginationFeature,
})
const columnHelper = createColumnHelper<typeof features, Person>()const features = tableFeatures({
rowSortingFeature,
rowPaginationFeature,
})
const columnHelper = createColumnHelper<typeof features, Person>()import type { StockFeatures } from '@tanstack/svelte-table'
type PersonColumn = ColumnDef<StockFeatures, Person>import type { StockFeatures } from '@tanstack/svelte-table'
type PersonColumn = ColumnDef<StockFeatures, Person>No more declaration merging required! (Although it still works if you want to keep using it)
Global declaration merging works exactly like it did in v8. The only change you need to make is updating the generics shape: both interfaces now take TFeatures as the first type parameter.
declare module '@tanstack/svelte-table' {
interface ColumnMeta<TFeatures, TData, TValue> {
align?: 'left' | 'right'
}
}declare module '@tanstack/svelte-table' {
interface ColumnMeta<TFeatures, TData, TValue> {
align?: 'left' | 'right'
}
}That's all that's required if you want to keep declaring meta types globally.
Optionally, v9 also adds a new way to declare meta types per-table without declaration merging. You can use type-only tableMeta/columnMeta slots on the features option, which only affect tables created with that features object:
const features = tableFeatures({
rowSortingFeature,
columnMeta: metaHelper<{ align?: 'left' | 'right' }>(),
})const features = tableFeatures({
rowSortingFeature,
columnMeta: metaHelper<{ align?: 'left' | 'right' }>(),
})See the new Table and Column Meta Guide for full details on both approaches.
In v8, making a custom function usable as a string reference (like filterFn: 'fuzzy') required declare module augmentation of the FilterFns interface, and typing filter meta required augmenting FilterMeta. In v9, registering the function in the matching registry slot does both jobs with no global augmentation:
// v8
declare module '@tanstack/svelte-table' {
interface FilterFns {
fuzzy: FilterFn<unknown>
}
interface FilterMeta {
itemRank: RankingInfo
}
}
// v9 - register in the slot; the key becomes a valid string value
interface FuzzyFilterMeta {
itemRank?: RankingInfo
}
const features = tableFeatures({
columnFilteringFeature,
filteredRowModel: createFilteredRowModel(),
filterFns: { ...filterFns, fuzzy: fuzzyFilter },
filterMeta: metaHelper<FuzzyFilterMeta>(),
})
// 'fuzzy' now typechecks in column defs for tables using these features
columnHelper.accessor('name', { filterFn: 'fuzzy' })// v8
declare module '@tanstack/svelte-table' {
interface FilterFns {
fuzzy: FilterFn<unknown>
}
interface FilterMeta {
itemRank: RankingInfo
}
}
// v9 - register in the slot; the key becomes a valid string value
interface FuzzyFilterMeta {
itemRank?: RankingInfo
}
const features = tableFeatures({
columnFilteringFeature,
filteredRowModel: createFilteredRowModel(),
filterFns: { ...filterFns, fuzzy: fuzzyFilter },
filterMeta: metaHelper<FuzzyFilterMeta>(),
})
// 'fuzzy' now typechecks in column defs for tables using these features
columnHelper.accessor('name', { filterFn: 'fuzzy' })The same pattern applies to sortFns (for sortFn string values) and aggregationFns (for aggregationFn string values). Once a custom function is registered in a registry slot, prefer the string reference in column defs (sortFn: 'fuzzy') over passing the function directly; svelte-check is stricter about function variance, and the string form sidesteps it. See the Fuzzy Filtering Guide for a complete example.
Prefer explicit object row types:
type Person = {
firstName: string
lastName: string
age: number
}type Person = {
firstName: string
lastName: string
age: number
}