Svelte Headless Table

A headless table is the part of a data grid that is not the DOM: which rows are visible after filtering, in what order after sorting, which are selected, which group is expanded, which cell is active. SvGrid ships that part on its own as @svgrid/grid/core. You call createSvGrid with data and columns, read rows and header groups back, and render them with whatever markup your design system already has.

Nothing on this subpath touches the DOM, ARIA or CSS. There is no stylesheet to override and no class name to fight, which is the reason platform teams with a strict internal component library reach for it: the engine owns the state, the design system owns the markup, and the two do not argue. The WAI-ARIA attribute factories and the virtualizers are exported alongside the engine, so an accessible, windowed table over 50,000 rows is still yours to draw.

The same engine drives the render-ready <SvGrid> component, so a team can start headless on the screens the design system covers and drop the component in where it does not. One state machine, two renderers, and the measured size of the engine alone is printed below rather than typed.

Install

npm i @svgrid/grid

Free and MIT-licensed in @svgrid/grid: no license key, no row cap, no watermark.

Measured: 2.6 KB gzipped JS, no stylesheet, minified with Svelte external, at @svgrid/grid 3.0.4 on 2026-09-20. Reproduce it with pnpm size:json; the method is on the bundle size reference.

The code

<script lang="ts">
  import {
    createSvGrid, createCoreRowModel, createSortedRowModel,
    tableFeatures, rowSortingFeature, type ColumnDef,
  } from '@svgrid/grid/core'

  const features = tableFeatures({ rowSortingFeature })
  const columns: ColumnDef<typeof features, Person>[] = [
    { field: 'name', header: 'Name' },
    { field: 'age', header: 'Age', editorType: 'number' },
  ]

  let sorting = $state([{ id: 'age', desc: true }])
  const table = createSvGrid({
    _features: features,
    _rowModels: { coreRowModel: createCoreRowModel(), sortedRowModel: createSortedRowModel() },
    data, columns,
    state: { sorting },
    onSortingChange: (u) => (sorting = typeof u === 'function' ? u(sorting) : u),
  })
  const rows = $derived(table.getRowModel().rows)
</script>

<table class="your-table">
  <thead>
    {#each table.getHeaderGroups() as hg (hg.id)}
      <tr>
        {#each hg.headers as h (h.id)}
          <th onclick={h.column.getToggleSortingHandler()}>{h.column.columnDef.header}</th>
        {/each}
      </tr>
    {/each}
  </thead>
  <tbody>
    {#each rows as r (r.id)}
      <tr><td>{r.original.name}</td><td>{r.original.age}</td></tr>
    {/each}
  </tbody>
</table>

What you get

Live examples

Documentation

Related articles

Frequently asked questions

What is a headless table in Svelte?

A table engine without a renderer: it computes which rows to show, in what order, with what selection and grouping state, and hands you plain data to render. SvGrid exposes its engine as @svgrid/grid/core for exactly this use.

How is this different from a headless-only table library?

The engine is the same idea. The difference is that SvGrid also ships a render-ready <SvGrid> component over the same state machine, so you can go headless where your design system demands its own markup and use the component everywhere else, without two libraries.

Does headless mean no accessibility?

No. The core entry exports the WAI-ARIA attribute factories the render component uses, plus an activeCell state and moveActiveCell for keyboard navigation. Spread the attributes onto your own elements and screen readers announce the same grid.

Can I use it with shadcn-svelte or my own component library?

Yes. The engine returns rows and header groups; render them with Table.Root, Table.Row and Table.Cell or any elements you like. The headless styling guide shows the shadcn-svelte layout and a keyboard-navigable bare table.

How big is the headless engine?

The measured gzipped size of the createSvGrid entry is printed on this page from the size ledger the build writes, with the version and the date it was measured. The full render component is a separate figure on the bundle size page.

Build something else

Get started · Browse the gallery · Pricing