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 (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>:

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.

import {
  createSvGrid,
  createCoreRowModel,
  createFilteredRowModel,
  createSortedRowModel,
  createPaginatedRowModel,
  createGroupedRowModel,
  createExpandedRowModel,
} from '@svgrid/grid'

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

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:

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

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

// 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.

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

const table = $derived.by(() => createSvGrid({
  _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),
} as never))

const rows = $derived(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:

const table = createSvGrid({
  _rowModels: { coreRowModel: createCoreRowModel<Row>() }, // core only
  data: serverPage.rows,   // already sorted/filtered/paged by the API
  columns,
  state,
} as never)

Track the sort/filter state via the change handlers and re-fetch on change - see Server-side data (load on demand) for a complete, runnable example with paging, sorting, filtering and a race-safe fetch.

See also