Quick Start
There are two ways to render a grid. Start with the shorthand props, and move to createDataGrid when you need to own the state.
The Shorthand Form
Pass rows, columns and a row id. Sorting, filtering and column operations are registered for
you; adding pageSize adds pagination on top, and floatingFilters draws a filter row under the header.
<script lang="ts">
import { DataGrid, type ColumnDef } from '@sv5ui/datagrid';
const columns: ColumnDef<Person>[] = [
{ id: 'name', header: 'Name', sortable: true, filter: 'text', flex: 1 },
{ id: 'email', header: 'Email', sortable: true, filter: 'text', flex: 1.4 },
{ id: 'team', header: 'Team', sortable: true, filter: 'set', width: 120 },
{ id: 'salary', header: 'Salary', sortable: true, filter: 'number',
align: 'right', width: 120, type: 'currency' }
];
</script>
<!-- pageSize turns on pagination; toolbar adds quick filter, chips,
export menu, column chooser and density toggle -->
<DataGrid
data={people}
{columns}
getRowId={(person) => String(person.id)}
pageSize={6}
toolbar
/>Selection
selection takes the defaults, or an options object. The checkbox column is pinned to the left edge and
is not a data column.
<!-- true takes the defaults: multiple selection with a checkbox column -->
<DataGrid data={people} {columns} {getRowId} selection pageSize={6} toolbar />
<!-- or configure it -->
<DataGrid
data={people}
{columns}
{getRowId}
selection={{ mode: 'single', checkbox: true }}
/>Virtualization
2,000 rows below, with a DOM that only ever holds the visible window plus a little overscan. Virtualization replaces pagination rather than joining it.
<!-- virtual replaces pagination. Give the grid a fixed height through
class, because the viewport is what decides the visible window. -->
<DataGrid
data={twoThousandPeople}
{columns}
{getRowId}
virtual
class="h-[400px]"
/>
<!-- with options -->
<DataGrid
data={rows}
{columns}
{getRowId}
virtual={{ rowHeight: 36, overscan: 8, columns: true }}
class="h-[400px]"
/>Loading, Error and Empty
Three replaceable surfaces. The skeleton fills the grid rather than showing a fixed few rows, and an error wins over both loading and data.
<!-- Skeleton rows. The count fills the viewport unless loadingRows says otherwise. -->
<DataGrid data={[]} {columns} {getRowId} loading pageSize={5} />
<!-- Error takes precedence over loading and rows. onRetry renders a Retry action. -->
<DataGrid data={[]} {columns} {getRowId} error="Could not reach the server" onRetry={reload} />
<!-- Empty state text -->
<DataGrid data={[]} {columns} {getRowId} emptyText="No members match this filter" />DataGrid Props
data, columns and getRowId belong to the shorthand form; grid replaces all three. TypeScript rejects a mix of the two.
| Prop | Default |
|---|---|
data | - |
columns | - |
getRowId | - |
grid | - |
pageSize | - |
virtual | - |
selection | - |
editing | - |
toolbar | false |
density | 'standard' |
rowClass | - |
loading | false |
loadingRows | fills the viewport |
error | null |
onRetry | - |
emptyText | labels.noData |
exportFilename | 'export.csv' |
fullWidthRow | - |
persistState | - |
ui | - |
class | - |
Owning the Grid
createDataGrid returns the grid as a plain object, so the same instance can be read, driven and passed around.
Only the features you list are imported.
<script lang="ts">
import {
createDataGrid,
DataGrid,
columnOps,
filtering,
pagination,
selection,
sorting
} from '@sv5ui/datagrid';
const grid = createDataGrid<Person>({
data: people,
columns,
getRowId: (person) => String(person.id),
density: 'compact',
features: [
sorting({ nulls: 'last' }),
filtering(),
columnOps({ pin: false }),
selection({ mode: 'multiple' }),
pagination({ pageSize: 6 })
]
});
</script>
<DataGrid {grid} toolbar />Reading State Back
Each feature exposes reactive state through its accessor and imperative methods through grid.api. The accessor returns undefined when the feature is not registered, which is why the calls
above are optional.
Every badge below is one $derived line over an accessor. Sort a column, tick a row, page: they follow, and nothing subscribes to
anything.
import { getSelection, getSorting } from '@sv5ui/datagrid';
// Feature state, reactive because it is built from runes
getSelection(grid)?.selectedIds;
getSorting(grid)?.sort;
// Imperative methods every registered feature merges into grid.api
grid.api.setSort?.([{ columnId: 'name', direction: 'asc' }]);
grid.api.clearSelection?.();
grid.api.exportCsv?.({ filename: 'members.csv' });
// The kernel's own two, always present
const snapshot = grid.api.getState();
grid.api.setState(snapshot);Events
A typed bus on the instance. This is what a server row model listens to, and what an autosaving grid hooks into.
Sort a column, filter, page, select a row or drag a column edge. Every event the grid emits lands here, newest first.
- No events yet.
<script lang="ts">
// A typed event bus. Every entry carries its own payload shape.
let log = $state<{ id: number; name: string; payload: string }[]>([]);
let id = 0;
function logEvent(name: string, payload: unknown) {
id += 1;
log = [{ id, name, payload: JSON.stringify(payload) }, ...log].slice(0, 5);
}
grid.events.on('sortChanged', (payload) => logEvent('sortChanged', payload));
grid.events.on('filterChanged', (payload) => logEvent('filterChanged', payload));
grid.events.on('pageChanged', (payload) => logEvent('pageChanged', payload));
grid.events.on('selectionChanged', (payload) => logEvent('selectionChanged', payload));
grid.events.on('columnResized', (payload) => logEvent('columnResized', payload));
grid.events.on('columnMoved', (payload) => logEvent('columnMoved', payload));
grid.events.on('columnPinned', (payload) => logEvent('columnPinned', payload));
grid.events.on('columnVisibilityChanged', (p) => logEvent('columnVisibilityChanged', p));
// The rest of the map: rowsCopied, rowExpanded, rowPinnedChanged,
// rowMoved, cellEdited, rowEdited.
</script>
<ul>
{#each log as entry (entry.id)}
<li><Badge label={entry.name} /> <code>{entry.payload}</code></li>
{/each}
</ul>createDataGrid Options
| Option | Description |
|---|---|
columns | Column definitions |
data | Rows for the client row model |
getRowId | Stable unique row id |
features | Feature modules to register. Order does not matter |
density | Row height and cell padding, through CSS variables |
rowModel | 'server' passes filter, sort and window through untouched |
locales | Languages the grid may use, chosen from the page language |
locale | BCP-47 tag forcing one of them |
labels | Overrides on top of the chosen language, any subset |
announcer | The same, for what the live region says |
rowClass | Classes added per rendered row |
Which Form to Use
Use the shorthand until you need to read the grid from outside the component, choose which features load, switch language in place, or drive a server row model. Moving between them changes the props, not the columns.
The shorthand registers sorting, filtering and column operations, plus pagination, selection, editing and virtualization when their props are set.
<!-- The two forms are mutually exclusive: pass grid, or pass
data + columns + getRowId. TypeScript rejects a mix. -->
<!-- Shorthand: the component owns the grid -->
<DataGrid data={people} {columns} {getRowId} pageSize={10} />
<!-- Instance: you own the grid, and everything about it -->
<DataGrid {grid} />