shadcn-svelte integration

If your Svelte 5 app already uses shadcn-svelte, SvGrid drops in without a redesign. You keep the exact palette, radius, font, and dark mode you already have - the grid reads them straight from the same CSS variables your Button, Input, and Table components use.

There is nothing to "port". A shadcn app stores its theme as a flat set of CSS custom properties (--background, --foreground, --border, --primary, ...). SvGrid themes off its own flat set of custom properties (--sg-*). Wiring the two together is one CSS block

Side by side: the same table, before and after

On the left is what a shadcn app already has - a hand-written Table. On the right is the same region rendered by <SvGrid>, which inherits the identical tokens and adds sorting, filtering, and virtualization for free. The only new lines are the import and the grid element.

Before - shadcn Table
<script lang="ts">
  import * as Table from '$lib/components/ui/table'
  let rows = [/* ...orders... */]
</script>

<Table.Root> <Table.Header> <Table.Row> <Table.Head>Order</Table.Head> <Table.Head>Customer</Table.Head> <Table.Head class="text-right">Amount</Table.Head> </Table.Row> </Table.Header> <Table.Body> {#each rows as r} <Table.Row> <Table.Cell>{r.id}</Table.Cell> <Table.Cell>{r.customer}</Table.Cell> <Table.Cell class="text-right">{r.amount}</Table.Cell> </Table.Row> {/each} </Table.Body> </Table.Root> <!-- static markup: no sort, no filter, no virtualization -->

After - <SvGrid>, same tokens
<script lang="ts">
  import { SvGrid, type ColumnDef } from '@svgrid/grid'
  import '@svgrid/grid/themes/shadcn.css'  // 1 line to match shadcn

let rows = [/* ...same orders... */] const columns: ColumnDef[] = [ { id: 'id', field: 'id', header: 'Order' }, { id: 'customer', field: 'customer', header: 'Customer' }, { id: 'amount', field: 'amount', header: 'Amount', type: 'number' }, ] </script>

<SvGrid data={rows} {columns} sortable filterable class="rounded-md border" /> <!-- sortable, filterable, virtualized, keyboard + a11y -->

The @svgrid/grid/themes/shadcn.css import above is the fastest path: a ready-made preset that mirrors shadcn's zinc-neutral palette in light and dark, toggled by the same [data-theme='dark'] attribute you already use. Import it once and you are done.

Install it with the CLI you already have

SvGrid publishes a shadcn-svelte registry item, so the grid installs the same way the rest of your components did:

The examples on this page run 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 data = people

  const columns: GridColumns<Person> = [
    { field: 'name',       header: 'Name',       width: 200 },
    { field: 'department', header: 'Department', width: 150 },
    { field: 'city',       header: 'City',       width: 140 },
    { field: 'age',        header: 'Age',        width: 90 },
    { field: 'salary',     header: 'Salary',     width: 130, format: { type: 'currency', currency: 'USD' } },
  ]
</script>
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/ and installs @svgrid/grid. Your components.json aliases are respected, and the file is yours - the difference from a copied recipe is that the behaviour behind it lives in a package you version-bump.

Already have the TanStack-based data table from the shadcn docs? See Migrating from the shadcn-svelte data table.

Inherit your exact theme with the live token bridge

The preset matches the default shadcn look. If you have customized your theme - a different --primary, a wider --radius, a brand font - skip the preset and bridge SvGrid's tokens to your variables instead. Now the grid tracks whatever your app's tokens resolve to, in real time.

shadcn stores its palette as bare HSL channels (--background holds 0 0% 100%, not hsl(0 0% 100%)), so wrap each one in hsl() at the point of assignment:

/* Scope to a wrapper so only grids inside it inherit the map.
 * Move it to :root in app.css to theme every grid on the site. */
.sg-shadcn {
  --sg-bg:                    hsl(var(--background));
  --sg-fg:                    hsl(var(--foreground));
  --sg-border:                hsl(var(--border));
  --sg-header-bg:             hsl(var(--muted));
  --sg-header-fg:             hsl(var(--foreground));
  --sg-row-alt-bg:            hsl(var(--muted) / 0.3);
  --sg-row-hover-bg:          hsl(var(--accent));
  --sg-selection-bg:          hsl(var(--primary) / 0.15);
  --sg-accent:                hsl(var(--primary));
  --sg-focus-ring:            0 0 0 2px hsl(var(--ring));
  --sg-scrollbar-bg:          hsl(var(--background));
  --sg-scrollbar-thumb:       hsl(var(--muted-foreground) / 0.4);
  --sg-scrollbar-thumb-hover: hsl(var(--muted-foreground) / 0.6);
  --sg-font:                  var(--font-sans, sans-serif);
  --sg-radius:                var(--radius);
}
<div class="sg-shadcn">
  <SvGrid {data} {columns} sortable filterable rowHeight={40} />
</div>

Row height is a prop, not a token - the virtualizer needs it as a number, so it cannot come from the stylesheet above.

Always wrap in hsl(). Writing --sg-accent: var(--primary) passes bare HSL channels straight to the grid, which expects a complete colour value - the result is an invalid colour (usually transparent).

Dark mode is automatic

shadcn already redefines its tokens under .dark. Because every --sg-* value above is expressed in terms of those tokens, the grid flips with the rest of your app the instant .dark is toggled on <html> - no listener, no reactive statement, no API call. SvGrid reads --sg-* from the DOM at paint time, so mid-session theme switches repaint the grid for free.

This also means nested themes work: put .dark on one panel and grids inside it go dark while the rest of the page stays light.

Custom cells with shadcn components

Cell snippets are plain Svelte, so you can render shadcn components directly inside the grid - a Badge for status, a Button for row actions:

{#snippet StatusCell({ value }: { value: string })}
  <Badge variant={value === 'delivered' ? 'default' : 'secondary'}>
    {value}
  </Badge>
{/snippet}

{#snippet RowActions({ row })}
  <Button variant="ghost" size="sm" onclick={() => edit(row)}>Edit</Button>
{/snippet}

Assign the snippet to a column's cell field ({ id: 'status', field: 'status', header: 'Status', cell: StatusCell }) and it renders in every row, inheriting the same theme context.

A note on OKLCH

Recent shadcn-svelte releases have been migrating tokens from bare HSL channels to OKLCH. If your app.css defines --primary as oklch(...), use oklch(var(--primary) / 0.15) in the bridge instead of hsl(...). Check the format of --background in your app.css before writing the map; open DevTools and confirm the computed value of --sg-bg is a real colour, not transparent, after any shadcn upgrade.

Frequently asked questions

Does SvGrid work with shadcn-svelte?

Yes. Import the @svgrid/grid/themes/shadcn.css preset for the default shadcn look, or bridge SvGrid's --sg-* tokens to your own --background / --foreground / --primary variables to inherit your exact customized theme. Both handle dark mode through the same .dark / [data-theme='dark'] toggle you already use.

Do I need to duplicate my theme for the grid?

No. The whole point of the token bridge is that there is one source of truth - your shadcn variables. The grid reads them; it does not copy them.

Will dark mode break?

No, as long as your bridge expresses --sg-* in terms of shadcn tokens (hsl(var(--background))) rather than hard-coded colours. The cascade handles the flip.

Try it

The grid reads --sg-* custom properties, so matching a design system is a block of variables rather than a theme file. Change one and the grid follows.

<div class="shadcn-scope">
  <SvGrid data={people} {columns} sortable />
</div>

<style>
  .shadcn-scope {
    --sg-accent: hsl(240 5.9% 10%);
    --sg-border: hsl(240 5.9% 90%);
    --sg-header-bg: hsl(240 4.8% 95.9%);
    --sg-row-hover: hsl(240 4.8% 97%);
    --sg-radius: 8px;
    --sg-font-size: 13px;
  }
</style>

Matching a light shadcn surface

The same grid under a light token block. Because the tokens are plain custom properties, a theme switch is a class change rather than a rebuild.

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

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

  const seed: Person[] = [
    { id: 1, name: 'Ada Lovelace',   city: 'London',   age: 36, salary: 142000 },
    { id: 2, name: 'Grace Hopper',   city: 'New York', age: 45, salary: 168000 },
    { id: 3, name: 'Linus Torvalds', city: 'Portland', age: 54, salary: 155000 },
  ]

  const columns: GridColumns<Person> = [
    { field: 'name',   header: 'Name',   width: 180 },
    { field: 'city',   header: 'City',   width: 150 },
    { field: 'salary', header: 'Salary', width: 150,
      format: { type: 'currency', currency: 'USD' } },
  ]
</script>

<div class="shadcn-light">
  <SvGrid data={seed} {columns} sortable />
</div>

<style>
  .shadcn-light {
    --sg-bg: hsl(0 0% 100%);
    --sg-fg: hsl(240 10% 3.9%);
    --sg-border: hsl(240 5.9% 90%);
    --sg-header-bg: hsl(240 4.8% 95.9%);
    --sg-header-fg: hsl(240 3.8% 46.1%);
    --sg-row-hover: hsl(240 4.8% 97.5%);
    --sg-accent: hsl(240 5.9% 10%);
    --sg-radius: 8px;
  }
</style>

See also

Related articles