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

# Row models

The engine turns your raw `data` into the rows you render by running it through
a **pipeline** of row models. You opt into the steps you need; everything else
is tree-shaken out.

```
data
 └─▶ coreRowModel        wrap each item as a Row
      └─▶ filteredRowModel   drop rows that fail the column filters
           └─▶ sortedRowModel     order by the sort state
                └─▶ groupedRowModel    build group parent rows + aggregates
                     └─▶ expandedRowModel   flatten only expanded groups
                          └─▶ paginatedRowModel   slice to the current page
```

Each step is lazy and reads from the [controlled state](https://svgrid.com/docs/help/headless/controlled-state/)
(`sorting`, `columnFilters`, `grouping`, `expanded`, `pagination`).

Flip the group-by control below and watch the pipeline change shape - the same
engine goes from a flat list to `grouped -> expanded` with per-group `sum`
aggregates, and the markup stays a plain hand-styled `<table>`:

> Live demo: Row models are a pipeline - https://svgrid.com/demos/190-headless-row-models/

## Opt in per step

Pass only the models you want into `_rowModels`. `createCoreRowModel` is always
required (it's the entry point); the rest are optional.

```ts
import {
  createSvGrid,
  createCoreRowModel,
  createFilteredRowModel,
  createSortedRowModel,
  createPaginatedRowModel,
  createGroupedRowModel,
  createExpandedRowModel,
  tableFeatures,
  rowPaginationFeature,
  columnGroupingFeature,
  rowExpandingFeature,
} from '@svgrid/grid/core'

const table = createSvGrid({
  _features: features,
  _rowModels: {
    coreRowModel:      createCoreRowModel<Row>(),
    filteredRowModel:  createFilteredRowModel<Row>(),
    sortedRowModel:    createSortedRowModel<Row>(),
    paginatedRowModel: createPaginatedRowModel<Row>(),
  },
  data,
  columns,
  state,
  // ...change handlers
})

const rows = table.getRowModel().rows // filtered → sorted → paged
```

Skip `paginatedRowModel` and `getRowModel().rows` returns every matching row.
Skip `filteredRowModel` and the `columnFilters` state is simply ignored.

## Pagination

Add `createPaginatedRowModel` and drive it with `pagination` state:

```ts
let pagination = $state({ pageIndex: 0, pageSize: 20 })

const features = tableFeatures({ rowPaginationFeature })

const table = $derived.by(() => createSvGrid({
  _features: features,
  _rowModels: {
    coreRowModel:      createCoreRowModel<Row>(),
    paginatedRowModel: createPaginatedRowModel<Row>(),
  },
  data, columns,
  state: { pagination },
  onPaginationChange: (u) =>
    (pagination = typeof u === 'function' ? u(pagination) : u),
}))

// getRowModel().rows is now just the current page
function nextPage() { pagination = { ...pagination, pageIndex: pagination.pageIndex + 1 } }
```

## Grouping + aggregation

`groupedRowModel` builds parent rows with aggregated values; `expandedRowModel`
then emits only the expanded children. A grouped row's `getIsGrouped()` is true
and its aggregates come from each column's `aggregate` setting.

```ts
let grouping = $state<string[]>(['lang'])
let expanded = $state<Record<string, boolean>>({})

const features = tableFeatures({ columnGroupingFeature, rowExpandingFeature })

const table = $derived.by(() => createSvGrid({
  _features: features,
  _rowModels: {
    coreRowModel:     createCoreRowModel<Repo>(),
    groupedRowModel:  createGroupedRowModel<Repo>(),
    expandedRowModel: createExpandedRowModel<Repo>(),
  },
  data,
  columns: [
    { field: 'lang', header: 'Lang' },
    { field: 'stars', header: 'Stars', aggregate: 'sum' }, // rolled up per group
  ],
  state: { grouping, expanded },
  onExpandedChange: (u) => (expanded = typeof u === 'function' ? u(expanded) : 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(() => {
  grouping
  expanded
  return table.getRowModel().rows
})
// each row: row.getIsGrouped(), row.getIsExpanded(), row.toggleExpanded()
```

## Server-side: skip the local pipeline

When the server does the sorting / filtering / paging, feed the engine only the
page it returned and leave those models out - the engine just wraps and renders
what you give it:

```ts
const table = createSvGrid({
  _features: tableFeatures({}),   // no features - the server did the work
  _rowModels: { coreRowModel: createCoreRowModel<Row>() }, // core only
  data: serverPage.rows,   // already sorted/filtered/paged by the API
  columns,
  state,
})
```

Track the sort/filter state via the change handlers and re-fetch on change - see
[Server-side data (load on demand)](https://svgrid.com/docs/help/headless/server-side/) for a complete, runnable
example with paging, sorting, filtering and a race-safe fetch.

## Pagination, running

`paginatedRowModel` is the last stage, so `getRowModel().rows` is already the
current page - there is no slicing left for you to do. Page state is yours, which
is why the buttons below just assign to it.

```svelte
<script lang="ts">
  import {
    createSvGrid,
    createCoreRowModel,
    createPaginatedRowModel,
    tableFeatures,
    rowPaginationFeature,
    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 },
    { name: 'tinygo',   lang: 'Go',         stars: 15000 },
    { name: 'bun',      lang: 'Zig',        stars: 74000 },
    { name: 'zig',      lang: 'Zig',        stars: 35000 },
  ]

  const features = tableFeatures({ rowPaginationFeature })

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

  let pagination = $state({ pageIndex: 0, pageSize: 3 })

  const table = $derived.by(() =>
    createSvGrid({
      _features: features,
      _rowModels: {
        coreRowModel: createCoreRowModel<Repo>(),
        paginatedRowModel: createPaginatedRowModel<Repo>(),
      },
      data,
      columns,
      state: { pagination },
      onPaginationChange: (u) => (pagination = typeof u === 'function' ? u(pagination) : 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(() => {
    pagination
    return table.getRowModel().rows
  })
  const pages = $derived(Math.ceil(data.length / pagination.pageSize))

  function go(delta: number) {
    const next = Math.min(pages - 1, Math.max(0, pagination.pageIndex + delta))
    pagination = { ...pagination, pageIndex: next }
  }
</script>

<div>
  <button type="button" onclick={() => go(-1)} disabled={pagination.pageIndex === 0}>Prev</button>
  <span>Page {pagination.pageIndex + 1} of {pages}</span>
  <button type="button" onclick={() => go(1)} disabled={pagination.pageIndex >= pages - 1}>Next</button>
</div>

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

## Grouping, running

`groupedRowModel` inserts parent rows; `expandedRowModel` decides which
children survive. A parent is the row where `getCanExpand()` is true, and its
aggregated cells come from the column's `aggregate` setting - so a group row and
a leaf row are two different shapes in the same list, and your markup has to
branch on it.

```svelte
<script lang="ts">
  import {
    createSvGrid,
    createCoreRowModel,
    createGroupedRowModel,
    createExpandedRowModel,
    tableFeatures,
    columnGroupingFeature,
    rowExpandingFeature,
    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 },
    { name: 'tinygo',   lang: 'Go',         stars: 15000 },
    { name: 'bun',      lang: 'Zig',        stars: 74000 },
    { name: 'zig',      lang: 'Zig',        stars: 35000 },
  ]

  const features = tableFeatures({ columnGroupingFeature, rowExpandingFeature })

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

  let grouping = $state<string[]>(['lang'])
  let expanded = $state<Record<string, boolean>>({})

  const table = $derived.by(() =>
    createSvGrid({
      _features: features,
      _rowModels: {
        coreRowModel: createCoreRowModel<Repo>(),
        groupedRowModel: createGroupedRowModel<Repo>(),
        expandedRowModel: createExpandedRowModel<Repo>(),
      },
      data,
      columns,
      state: { grouping, expanded },
      onExpandedChange: (u) => (expanded = typeof u === 'function' ? u(expanded) : 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(() => {
    grouping
    expanded
    return table.getRowModel().rows
  })

  // Feature-conditional members: present because rowExpandingFeature is on.
  const isGroup = (r: any) => typeof r.getCanExpand === 'function' && r.getCanExpand()
</script>

<table>
  <tbody>
    {#each rows as r (r.id)}
      {#if isGroup(r)}
        <tr onclick={() => (r as any).toggleExpanded?.()} style="cursor: pointer; font-weight: 600;">
          <td colspan="2">
            {(r as any).getIsExpanded?.() ? '- ' : '+ '}{(r as any).getCellValueByColumnId('lang')}
          </td>
          <td>{Number((r as any).getCellValueByColumnId('stars') ?? 0).toLocaleString()}</td>
        </tr>
      {:else}
        {@const repo = r.original as Repo}
        <tr>
          <td style="padding-left: 18px;">{repo.name}</td>
          <td>{repo.lang}</td>
          <td>{repo.stars.toLocaleString()}</td>
        </tr>
      {/if}
    {/each}
  </tbody>
</table>
```

## See also

- [Build a table from scratch](https://svgrid.com/docs/help/headless/build-a-table/)
- [Server-side data (load on demand)](https://svgrid.com/docs/help/headless/server-side/)
- [Controlled state](https://svgrid.com/docs/help/headless/controlled-state/)
- [Grouping & aggregation](https://svgrid.com/docs/help/grouping-aggregation/)

---

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