Migrating from the shadcn-svelte data table

The shadcn-svelte Data Table is not the same thing as its <Table> component. <Table> is presentation only. The data table is a recipe built on TanStack Table v9 that you copy into your own repo: it already does sorting, filtering, pagination, row selection, and column visibility. If that is what you have, this is your page. If you have the plain styled <Table> and hand-wrote the sorting yourself, read Migrating from a UI-kit table instead.

Estimated effort: 20-40 min per table. Your column definitions and your feature registration carry over almost unchanged. Most of the work is deleting files.

Facts, checked 12 Sep 2026. @tanstack/svelte-table 9.2.4, MIT, last published 28 Aug 2026, 231,000 npm downloads in the 30 days to 10 Sep 2026. @svgrid/grid 3.0.3, MIT, last published 11 Sep 2026, 16,900 npm downloads in the same window. Bundle, minified and gzipped, each package built alone with Svelte external: SvGrid 3.0.4 97.6 KB JS + 10.5 KB CSS (measured 22 Sep 2026); @tanstack/svelte-table 9.2.4 36.3 KB JS, no separate stylesheet, svelte external (measured 12 Sep 2026). TanStack Table pricing, as its site states it: TanStack Table is MIT and free; tanstack.com is sponsor-supported and offers Enterprise Support as private consulting and expert support, with no licence sold for the table (https://tanstack.com/table/latest, read 12 Sep 2026). Flowbite / Skeleton / shadcn-svelte tables pricing, as its site states it: Flowbite Svelte, Skeleton and shadcn-svelte are MIT component libraries; their table components are free and no grid licence is sold (https://github.com/themesberg/flowbite-svelte, read 12 Sep 2026). SvGrid: MIT core; @svgrid/enterprise from $299 per developer per year (Indie, under $1M revenue), $599 for Enterprise. Side by side, with sources: SvGrid vs TanStack Table (Svelte), SvGrid vs Flowbite / Skeleton / shadcn-svelte tables.

Why this port is small

SvGrid speaks the same vocabulary TanStack Table v9 introduced. These are real exports from @svgrid/grid, not lookalikes:

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>
import {
  tableFeatures,
  rowSortingFeature,
  columnFilteringFeature,
  rowPaginationFeature,
  rowSelectionFeature,
  rowExpandingFeature,
  columnGroupingFeature,
} from '@svgrid/grid'

So data-table-features.ts moves over as-is, and columns.ts keeps its shape. What does not move is the rendering scaffolding, because SvGrid ships a renderer and there is nothing to translate it into.

What happens to each file

The shadcn recipe generates seven files. Five of them stop existing.

Their file Becomes
data-table-features.ts Kept. Same tableFeatures({...}) call.
columns.ts Kept, minus the header render helpers. { accessorKey: 'email' } becomes { field: 'email' }.
data-table.svelte Deleted. It is <table> markup over getHeaderGroups() / getRowModel(), which <SvGrid> does.
data-table-checkbox.svelte Deleted. showRowSelection renders the column and the select-all header.
data-table-email-button.svelte Deleted. sortable makes every header a sort toggle.
data-table-actions.svelte Kept if you want it, as a cell snippet.
+page.svelte Kept. It now renders one component instead of wiring state.

Let the codemod do the mechanical part

npx @svgrid/migrate src/routes/payments        # preview
npx @svgrid/migrate src/routes/payments --write # apply

It previews by default and only writes with --write. Point it at the whole route directory rather than a single file: the shadcn layout keeps features in its own module, and the codemod follows that import to work out which props the component needs.

It renames the column keys, re-points the imports, strips the features SvGrid does not have, and deletes the markup along with the $state / onXChange pairs. Custom cell and header render functions are kept and reported, never guessed at. Run your type-checker afterwards - the markup is regenerated rather than edited.

Before and after

- <script lang="ts">
-   import { createSvelteTable, FlexRender } from '$lib/components/ui/data-table'
-   import { columns } from './columns'
-   import { features } from './data-table-features'
-
-   let sorting = $state([])
-   let filters = $state([])
-   let selection = $state({})
-   let pagination = $state({ pageIndex: 0, pageSize: 10 })
-
-   const table = createSvelteTable({
-     get data() { return data },
-     columns, features,
-     state: { get sorting() { return sorting }, /* ...and three more... */ },
-     onSortingChange: (u) => (sorting = typeof u === 'function' ? u(sorting) : u),
-     // ...one of these per piece of state...
-   })
- </script>
-
- <Table.Root>
-   <Table.Header>
-     {#each table.getHeaderGroups() as headerGroup}
-       <!-- ...twenty more lines of markup... -->
-     {/each}
-   </Table.Header>
-   <Table.Body>
-     {#each table.getRowModel().rows as row}
-       <!-- ...and twenty more... -->
-     {/each}
-   </Table.Body>
- </Table.Root>

+ <script lang="ts">
+   import { SvGrid } from '@svgrid/grid'
+   import '@svgrid/grid/themes/shadcn.css'
+   import { columns } from './columns'
+   import { features } from './data-table-features'
+ </script>
+
+ <SvGrid
+   {data} {columns} {features}
+   sortable filterable showGlobalFilter showRowSelection
+   pageable showPagination pageSize={10}
+   fitColumns
+ />

The four $state declarations and their four onXChange updaters go away with the markup. Sorting, filtering, paging, and selection state live in the grid.

Column visibility moves too: mark a column visible: false to start it hidden, and users toggle it from the header column menu. You no longer maintain a dropdown for it.

One command

Use the CLI you already have:

npx shadcn-svelte@latest add https://svgrid.com/r/data-table.json

That writes a ready-to-edit data-table.svelte into $lib/components/ui/data-table/, beside your other components, and installs @svgrid/grid. It is a normal registry item, so components.json aliases and your existing setup are respected.

If you are not in a shadcn project, the same component ships through SvGrid's own scaffolder:

npx @svgrid/ui add data-table

Both routes write the identical file. It is yours to edit, exactly like a shadcn recipe. The difference is where the engine lives.

The upgrade path is the real difference

Both approaches put a file you own in your repo. Only one puts the engine in a package.

When TanStack Table went from v8 to v9, useReactTable became useTable, the get*RowModel() options became a features object, and flexRender became a component. Every copied data-table had to be hand-fixed, because there is no npm update for code you pasted. With SvGrid the file you own stays a thin call site, and the behaviour behind it updates with a version bump.

The cost scales with how much you pasted. A seven-file starter is an afternoon. The richer community grids paste a whole subsystem - row rendering, virtualization, context menus, paste dialogs, hooks, config - and every one of those files is now yours to carry through the next upstream change.

Keep your design system

You do not have to leave shadcn's look, and you do not have to leave its markup either.

What you gain

The official recipe stops where its docs stop: sorting, filtering, pagination, row selection, column visibility. Community registry items go further, at the cost of pasting a whole multi-file subsystem into your repo. In SvGrid each of these is a prop:

What you give up

Frequently asked questions

Does SvGrid replace shadcn-svelte?

No. It replaces one recipe in it. Your buttons, dialogs, and inputs stay exactly as they are, and the grid reads the same theme variables they do.

Can I keep my existing columns.ts?

Mostly. Rename accessorKey to field, drop the header render helpers that only existed to draw a sort button, and the rest carries over. Custom cells become snippets via renderSnippet.

Do I have to give up owning the code?

You still own the call site - npx @svgrid/ui add data-table writes a file into your project that you edit freely. What you stop owning is the row-model plumbing, which is the part that broke on the v8 to v9 upgrade.

Is this worth it for a 50-row table?

Probably not, unless you want editing, grouping, or export later. The case gets strong when the table grows past what fits on a page, or when the next feature request is one you would otherwise hand-build.

What you end up with

Sorting, filtering, pagination and selection - the shadcn data-table demo, as props.

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

See also

Related articles