DataGrid v1.3.0

DataGrid for Svelte 5

@sv5ui/datagrid is a separate package built on sv5ui: a virtualized, keyboard-navigable data grid assembled from feature modules you can also write yourself.

Headless core

createDataGrid returns runes and a pure row pipeline. No DOM, no styling, usable on its own.

Feature modules

Nine opt-in modules built on the same hooks your own features get. Unregistered means unbundled.

Virtualized

Past a million rows, with a DOM node count that does not grow with the data.

One tab stop

ARIA grid and treegrid, roving tabindex, a live announcer, and full keyboard navigation.

Basic Example

Three props and a grid: rows, columns and a stable row id. The shorthand form registers sorting, filtering, column operations and pagination for you.

Hoang Kowalski
hoang.kowalski1@example.com
Design
Manager
$127,691.00
Bruno Nguyen
bruno.nguyen2@example.com
Growth
Support
$105,146.00
Bruno Dubois
bruno.dubois3@example.com
Design
Manager
$86,538.00
Farid Haddad
farid.haddad4@example.com
Growth
Analyst
$76,443.00
Jonas Yilmaz
jonas.yilmaz5@example.com
Core
Support
$129,812.00
Quyen Tanaka
quyen.tanaka6@example.com
Growth
Analyst
$72,011.00
Mai Nguyen
mai.nguyen7@example.com
Support
Analyst
$83,226.00
Bruno Novak
bruno.novak8@example.com
Support
Analyst
$71,507.00
40 rows
1–8 of 40
<script lang="ts">
  import { DataGrid, type ColumnDef } from '@sv5ui/datagrid';

  interface Person {
    id: number;
    name: string;
    email: string;
    team: string;
    salary: number;
  }

  const people: Person[] = [/* ... */];

  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>

<DataGrid
  data={people}
  {columns}
  getRowId={(person) => String(person.id)}
  pageSize={8}
  toolbar
/>

Two Layers

The package is a headless core and a component layer, either usable on its own.

The headless core

createDataGrid returns a GridState: Svelte 5 runes and a derived row pipeline. Use it when you want to choose the features, hold the state, or drive the grid from outside.

0 selected sort: none
Hoang Kowalski
hoang.kowalski1@example.com
Design
Manager
$127,691.00
Bruno Nguyen
bruno.nguyen2@example.com
Growth
Support
$105,146.00
Bruno Dubois
bruno.dubois3@example.com
Design
Manager
$86,538.00
Farid Haddad
farid.haddad4@example.com
Growth
Analyst
$76,443.00
Jonas Yilmaz
jonas.yilmaz5@example.com
Core
Support
$129,812.00
40 rows
1–5 of 40
<script lang="ts">
  import {
    createDataGrid,
    DataGrid,
    columnOps,
    filtering,
    getSelection,
    getSorting,
    pagination,
    selection,
    sorting
  } from '@sv5ui/datagrid';

  // Only these five features are imported, so only these five are bundled.
  // Name the row type on createDataGrid and the factories follow from it.
  const grid = createDataGrid<Person>({
    data: people,
    columns,
    getRowId: (person) => String(person.id),
    features: [
      sorting(),
      filtering(),
      columnOps(),
      selection(),
      pagination({ pageSize: 5 })
    ]
  });

  // Plain derived values: no subscription, no store, no event handler
  let sort = $derived(
    getSorting(grid)
      ?.sort.map((entry) => `${entry.columnId} ${entry.direction}`)
      .join(', ') || 'none'
  );
</script>

<Badge label="{getSelection(grid)?.count ?? 0} selected" />
<Badge label="sort: {sort}" />

<Button
  label="Sort by salary"
  onclick={() => getSorting(grid)?.setSort([{ columnId: 'salary', direction: 'desc' }])}
/>
<Button label="Select every row" onclick={() => getSelection(grid)?.selectAll()} />

<DataGrid {grid} />

The component layer

DataGrid renders the whole thing. The Grid parts let you compose the chrome yourself, in any order, with your own markup between them.

Hoang Kowalski
hoang.kowalski1@example.com
Design
Manager
$127,691.00
Bruno Nguyen
bruno.nguyen2@example.com
Growth
Support
$105,146.00
Bruno Dubois
bruno.dubois3@example.com
Design
Manager
$86,538.00
Farid Haddad
farid.haddad4@example.com
Growth
Analyst
$76,443.00
Jonas Yilmaz
jonas.yilmaz5@example.com
Core
Support
$129,812.00
40 rows
1–5 of 40
<script lang="ts">
  import { Grid, createDataGrid } from '@sv5ui/datagrid';

  const grid = createDataGrid<Person>({ data, columns, getRowId, features });
</script>

<!-- The same grid, assembled by hand -->
<Grid.Root {grid} class="space-y-4">
  <Grid.Toolbar>
    <Grid.QuickFilter placeholder="Search members" />
    <Grid.FilterChips />
    <div class="ms-auto flex gap-2">
      <Grid.ExportMenu filename="members.csv" />
      <Grid.ColumnChooser />
      <Grid.DensityToggle />
    </div>
  </Grid.Toolbar>

  <Grid.Viewport>
    <Grid.Header />
    <Grid.Body />
  </Grid.Viewport>

  <Grid.StatusBar />
  <Grid.Pagination pageSizes={[5, 10, 25]} />
</Grid.Root>

The Row Pipeline

Every feature that changes which rows are shown does it by inserting a pure transform at a declared order. That is also how you read intermediate results back.

sourceNodes

40

before filtering

preWindowNodes

40

filtered and sorted

nodes

5

on screen

totalRows

40

what the footer counts

Hoang Kowalski
Design
$127,691.00
Bruno Nguyen
Growth
$105,146.00
Bruno Dubois
Design
$86,538.00
Farid Haddad
Growth
$76,443.00
Jonas Yilmaz
Core
$129,812.00
40 rows
1–5 of 40

Type above and only two numbers move: the source is untouched, and what is on screen is capped by the page size. Sorting moves none of them, because it reorders rows rather than removing them.

<script lang="ts">
  import { createDataGrid, DataGrid, filtering, getFiltering,
           pagination, sorting } from '@sv5ui/datagrid';

  // Every stage is a pure transform of RowNode[], inserted at a declared
  // order, so a stage never has to know what else is registered:
  //
  //   data -> filter (100) -> sort (200) -> group (300) -> flatten (400)
  //        -> pin-split (500) -> window (900) -> rendered rows
  //
  // Each link is memoized with $derived, so a stage whose input did not
  // change is not recomputed.
  const grid = createDataGrid<Person>({
    data: people,
    columns,
    getRowId,
    features: [sorting(), filtering(), pagination({ pageSize: 5 })]
  });

  // Page size belongs to the footer the grid already renders; only the
  // filter needs a control of its own, since this grid carries no toolbar.
  let query = $state('');

  // Every grid setter runs inside the library's mutator helper, so the state
  // it reads on its way out does not become a dependency of the caller.
  // Calling one straight from an effect is safe.
  $effect(() => {
    getFiltering(grid)?.setQuickFilter(query);
  });

  // The output is readable at any point along the chain, and each one is a
  // rune, so a derived value tracks it without a subscription.
  let stages = $derived([
    { label: 'sourceNodes', value: grid.sourceNodes.length, note: 'before filtering' },
    { label: 'preWindowNodes', value: grid.preWindowNodes.length, note: 'filtered and sorted' },
    { label: 'nodes', value: grid.nodes.length, note: 'on screen' },
    { label: 'totalRows', value: grid.totalRows, note: 'what the footer counts' }
  ]);
</script>

<Input bind:value={query} placeholder="Filter these rows" />

{#each stages as stage (stage.label)}
  <div class="rounded-lg border border-outline-variant p-3">
    <code>{stage.label}</code>
    <p class="text-xl font-semibold">{stage.value}</p>
    <p class="text-xs">{stage.note}</p>
  </div>
{/each}

<DataGrid {grid} />

Feature Modules

Opt-in and tree-shakeable. A feature you do not register is never imported, so its code stays out of your bundle.

FeatureAdds
sorting()Multi-sort with priority badges, per-type comparators, null ordering, sortFn and sortField
filtering()Quick filter plus text, number, date, set and boolean column filters, two conditions per column, a filter row under the header, chips
columnOps()Resize, reorder, fold header groups, pin left and right, hide, autosize, column menu and chooser
selection()Single or multi selection, checkbox column, select-all, Shift range, TSV copy, CSV export
editing()Cell and row editing with ten editors, schema validation, transactions, undo and redo, paste
pagination()Client paging plus the hooks a server row model needs
virtualization()Row and column virtualization, fixed or measured row heights
rowPinning()Rows pinned to the top or the bottom, outside filter and sort
rowReorder()Pointer and keyboard row reorder with an auto-scrolling drag preview

Performance

Measured on Chromium at a 1500x950 viewport with 39 columns of mixed renderers. The DOM node count is the number worth reading: it is the same at a million rows as at a hundred thousand, because only the visible window is rendered.

Sorting and filtering are benched apart from these, being arithmetic rather than rendering: at 100k rows across four columns a numeric sort is 18ms, a string sort 265ms, and a quick filter keystroke 6ms. The performance page carries both tables and what they leave out.

Metric100k rows500k rows1M rows
Data into the grid219ms251ms416ms
JS heap100MB315MB472MB
DOM nodes779779779
Scroll, median frame19ms23ms35ms

DataGrid or Table?

Both ship in the sv5ui ecosystem. The Table component is markup; the DataGrid is a data pipeline with a viewport.

What you needUse
A handful of static rows with custom markupTable
Sorting, filtering and paging over a few hundred rowsDataGrid
Thousands of rows in a scroll viewportDataGrid
Inline editing with validationDataGrid
Server-driven filter, sort and pagingDataGrid

API Stability

Everything exported from the package root is public and covered by semver. This is version 1.3.0, so a breaking change to any of it waits for 2.0. Internal helpers such as pipeline transforms, filter compilation, undo plumbing and column sizing maths stay unexported and change freely between releases.

Row grouping, tree data, master and detail rows, range selection and infinite scroll are not in this package. They are planned for a separate pro package.

SurfacePromise
Exports from the package rootPublic, and covered by semver
The two data attributes, data-dg-cell and data-dg-row-idPublic. Delegate from a wrapper and they will be there
The ui slot names and the density CSS variablesPublic. A slot may be added; one in use will not vanish silently
Class names, element nesting, the body transformInternal. Free to change between releases
Pipeline transforms, filter compilation, undo plumbing, sizing mathsInternal, and unexported. Needing one is a gap in the extension points