<!-- https://svgrid.com/docs/help/headless/overview/ - SvGrid documentation as markdown. Index of every page: https://svgrid.com/llms.txt -->

# Headless overview

`<SvGrid>` is the **renderer**. `createSvGrid` is the **engine** that powers it.
They're independent: you can use the engine on its own to build a custom UI,
render a plain `<table>` for print or email, run the row pipeline in a Web
Worker or on a server, or unit-test sort / filter / aggregation logic with no
DOM at all.

![The createSvGrid engine holds state and prop-getters, while the <SvGrid> renderer is one opinionated table built on that same engine.](https://svgrid.com/docs-media/grid-headless.svg)

```
your data ─▶ createSvGrid (engine) ─▶ row model ─▶ your markup
                     ▲                                  │
                controlled state  ◀──── change events ──┘
```

## What the engine gives you

`createSvGrid(options)` returns a table object with:

- `getRowModel(): { rows: Row<TData>[] }` - the final, post-pipeline rows
  (filtered → sorted → grouped → expanded → paginated),
- `getHeaderGroups(): HeaderGroup<TData>[]` - the multi-level column-header tree,
- `getAllColumns(): Column<TData>[]` - every column with its metadata (id,
  visible, pinned, width),
- imperative setters (`setColumnFilters`, `setPagination`, `setGrouping`,
  `setExpanded`, `setRowSelection`, `setActiveCell`) that push into the engine's
  store. Sorting has no setter - drive it from the `sorting` state you pass in,
  or from a header's `getToggleSortingHandler()`.

No DOM, no CSS, no virtualization - those live in the renderer. Here's the
engine rendering a plain, hand-styled `<table>` (sort + filter are the engine's;
the markup is the demo's):

> Live demo: Headless -> your own table - https://svgrid.com/demos/186-headless-table/

## When to reach for headless

| You want to… | Use |
| --- | --- |
| A rich grid in a Svelte app | `<SvGrid>` (start here) |
| Render as a plain `<table>` (print / email / RSC) | Headless |
| Drive a server-side row model from Node | Headless, via `createSvGridCore` |
| Unit-test sort / filter / aggregator logic | Headless |
| Build a custom virtualized renderer | Headless + the [virtualizer](https://svgrid.com/docs/help/headless/virtualization/) exports |
| Share one state object across two grids | Headless + [`createGridState`](https://svgrid.com/docs/help/headless/controlled-state/) |

For the common case, use [`<SvGrid>`](https://svgrid.com/docs/getting-started/2-first-grid/) - it wires all
of this for you. Reach for the engine when you need a different renderer or to
run the pipeline where there is no DOM.

## The three ideas

1. **Row models are a pipeline.** You opt into the steps you need
   (`coreRowModel`, `filteredRowModel`, `sortedRowModel`, …); unused steps are
   tree-shaken. See [Row models](https://svgrid.com/docs/help/headless/row-models/).
2. **State is controlled.** You pass `state` in and get `onXxxChange` events
   out - the engine never mutates your state. This is what makes it click with
   Svelte 5 `$state`. See [Controlled state](https://svgrid.com/docs/help/headless/controlled-state/).
3. **Rendering is yours.** The engine hands you rows + header groups; you emit
   the markup. See [Build a table from scratch](https://svgrid.com/docs/help/headless/build-a-table/).

## `createSvGrid` vs `createSvGridCore`

Both build the same engine and expose the same `getRowModel()` /
`getHeaderGroups()` surface. The difference is reactivity:

| | `createSvGrid` | `createSvGridCore` |
| --- | --- | --- |
| State | Svelte 5 runes | plain objects |
| Needs the Svelte compiler | Yes | No |
| Runs under | Vite, SvelteKit, vitest | anywhere Node runs |

Inside a component, use `createSvGrid` - runes are what make `$derived` re-run
the pipeline when your state changes. Outside one - a Node service, a worker, a
CLI, a plain unit test with no Svelte in the pipeline - use `createSvGridCore`
and rebuild it yourself when the state changes:

```js
// plain node script.mjs - no bundler, no compiler
import {
  createSvGridCore,
  createCoreRowModel,
  createSortedRowModel,
  tableFeatures,
  rowSortingFeature,
} from '@svgrid/grid/core'

const features = tableFeatures({ rowSortingFeature })
const table = createSvGridCore({
  _features: features,
  _rowModels: {
    coreRowModel: createCoreRowModel(),
    sortedRowModel: createSortedRowModel(),
  },
  data,
  columns,
  state: { sorting: [{ id: 'salary', desc: true }] },
  onSortingChange: () => {},
})

const rows = table.getRowModel().rows   // sorted, no DOM involved
```

`createSvGrid` imported into a bare Node process throws
`ReferenceError: $state is not defined` - that is the compiler missing, not a
bug. Reach for the core function there.

## The engine with no grid

No `<SvGrid>` anywhere. The engine owns sorting and hands back a row model;
every element below is markup you wrote, which is the whole proposition.

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

  type Repo = { name: string; lang: string; stars: number }

  const data: Repo[] = [
    { name: 'svelte',   lang: 'JavaScript', stars: 78000 },
    { name: 'vite',     lang: 'TypeScript', stars: 68000 },
    { name: 'sv-grid',  lang: 'TypeScript', stars: 172 },
    { name: 'rollup',   lang: 'JavaScript', stars: 25000 },
    { name: 'esbuild',  lang: 'Go',         stars: 38000 },
  ]

  const features = tableFeatures({ rowSortingFeature })

  const columns: ColumnDef<typeof features, Repo>[] = [
    { field: 'name',  header: 'Repo' },
    { field: 'lang',  header: 'Language' },
    { field: 'stars', header: 'Stars' },
  ]

  let sorting = $state([{ id: 'stars', desc: true }])

  const table = createSvGrid({
    _features: features,
    _rowModels: {
      coreRowModel: createCoreRowModel<Repo>(),
      sortedRowModel: createSortedRowModel<Repo>(),
    },
    data,
    columns,
    state: { sorting },
    onSortingChange: (u) => (sorting = typeof u === 'function' ? u(sorting) : u),
  })

  // Touch the state this component owns so the derived re-runs:
  // the engine's store is framework-free and not a rune.
  const rows = $derived.by(() => {
    sorting
    return table.getRowModel().rows
  })
</script>

<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)}
      {@const repo = r.original as Repo}
      <tr>
        <td>{repo.name}</td>
        <td>{repo.lang}</td>
        <td>{repo.stars.toLocaleString()}</td>
      </tr>
    {/each}
  </tbody>
</table>
```

## Adding a stage

Each row model is a pipeline stage you opt into. Register the filtered model
and a filter starts applying; leave it out and the code for it never ships.

```svelte
<script lang="ts">
  import {
    createSvGrid,
    createCoreRowModel,
    createFilteredRowModel,
    tableFeatures,
    columnFilteringFeature,
    type ColumnDef,
  } from '@svgrid/grid/core'

  type Repo = { name: string; lang: string; stars: number }

  const data: Repo[] = [
    { name: 'svelte',   lang: 'JavaScript', stars: 78000 },
    { name: 'vite',     lang: 'TypeScript', stars: 68000 },
    { name: 'sv-grid',  lang: 'TypeScript', stars: 172 },
    { name: 'rollup',   lang: 'JavaScript', stars: 25000 },
    { name: 'esbuild',  lang: 'Go',         stars: 38000 },
  ]

  const features = tableFeatures({ columnFilteringFeature })

  const columns: ColumnDef<typeof features, Repo>[] = [
    { field: 'name',  header: 'Repo' },
    { field: 'lang',  header: 'Language' },
  ]

  let columnFilters = $state<Array<{ id: string; value: unknown }>>([])

  const table = createSvGrid({
    _features: features,
    _rowModels: {
      coreRowModel: createCoreRowModel<Repo>(),
      filteredRowModel: createFilteredRowModel<Repo>(),
    },
    data,
    columns,
    state: { columnFilters },
    onColumnFiltersChange: (u) =>
      (columnFilters = typeof u === 'function' ? u(columnFilters) : u),
  })

  // Touch the state this component owns so the derived re-runs:
  // the engine's store is framework-free and not a rune.
  const rows = $derived.by(() => {
    columnFilters
    return table.getRowModel().rows
  })
</script>

<input
  placeholder="Filter language"
  oninput={(e) => (columnFilters = [{ id: 'lang', value: e.currentTarget.value }])}
/>

<ul>
  {#each rows as r (r.id)}
    {@const repo = r.original as Repo}
    <li>{repo.name} - {repo.lang}</li>
  {/each}
</ul>
```

## See also

- [Build a `<table>` from scratch](https://svgrid.com/docs/help/headless/build-a-table/) - a complete 30-line renderer
- [Styling a headless table](https://svgrid.com/docs/help/headless/styling/) - three looks from one engine
- [Row models](https://svgrid.com/docs/help/headless/row-models/) - the pipeline, step by step
- [Server-side data](https://svgrid.com/docs/help/headless/server-side/) - paging, sorting, filtering, load on demand
- [Controlled state](https://svgrid.com/docs/help/headless/controlled-state/) - `createGridState` / `subscribeGrid`
- [Headless virtualization](https://svgrid.com/docs/help/headless/virtualization/) - render 100k rows yourself
- [Why headless?](https://svgrid.com/docs/why-headless/) - the design rationale
- [Svelte headless table](https://svgrid.com/svelte/headless-table/) - the landing page, with the measured size of the engine
- [Bundle size](https://svgrid.com/docs/help/bundle-size/) - every entry, measured on each release
- [Architecture](https://svgrid.com/docs/help/architecture/) - the three-layer model

---

SvGrid is the Svelte 5 data grid (`npm install @svgrid/grid`, MIT). This page: https://svgrid.com/docs/help/headless/overview/ . All docs: https://svgrid.com/llms.txt
