Skip to content

Data Grid

A composable, virtualized spreadsheet grid for React. Copy the source into your project. Same model as shadcn/ui.

Open in
Stress test:
Paste a spreadsheet (Ctrl/Cmd+V) to fill rows
Current Table State
Live view of table + grid state for demonstration
Search Query:None
Total Rows:500
Selected Rows:0
Active Filters:0
Sorting:None
Hidden Columns:0
Pinned Columns:1 Left, 0 Right
Dynamic Columns:10
Focused Cell:None
Editing Cell:None
Selection Anchor:None
Can Undo / Redo:No / No
Last Commit:None

Keyboard nav, clipboard, fill, filters, dynamic columns, and typed editors, assembled from opt-in children.

Turn a Niko Table into an editable spreadsheet. You get virtualization, sort, filter, pin, and resize from the table, plus a headless edit engine: focus, selection, undo/redo, and clipboard.

  • Composable: mount only the features you need as children of <DataGrid>
  • Id-addressed: focus and edits use row id, so sort/filter stay correct
  • Performance-first: O(1) cell commits, virtualized body, no listeners for unmounted features
  • Own the code: copy via the registry; nothing opaque to unwrap
  1. Add the registry under registries in components.json (merge with your existing config):

    components.json
    {
    "registries": {
    "@niko-table": "https://niko-table.com/r/{name}.json"
    }
    }
  2. Install the grid (pulls in data-table, virtualization, and column resize):

    pnpm dlx shadcn@latest add @niko-table/data-table-grid

Manual copy-paste: Installation Guide.

Two contexts, two slots:

  • Table chrome (useDataTable): search, faceted filters, sort, view, export, pagination. Sit anywhere under DataTableRoot (sibling of <DataGrid> or nested inside it).
  • Grid pieces (useDataGridContext): undo/redo, add rows, clipboard, fill, move, reorder. Must be children of <DataGrid>.
DataTableRoot ← data={grid.rows}
├─ Search / Faceted / Sort / Clear / View / … ← table chrome (Root is enough)
└─ DataGrid ← keyboard, selection, scroll-into-view
├─ DataGridToolbar (Undo, Add rows, …) ← grid chrome (needs DataGrid)
├─ DataGridClipboard / Fill / Move / Reorder ← grid opt-ins
└─ DataTable
├─ VirtualizedHeader
└─ VirtualizedBody
└─ DataGridCell → GridTextCell / …
Layer Where Role
useDataGrid Call site Headless engine: rows, focus, edit, undo/redo
Table chrome Under DataTableRoot Filters / sort / view / pagination
<DataGrid> Under DataTableRoot Display-order nav + selection
Grid chrome + opt-ins Inside <DataGrid> Undo, add rows, clipboard, fill, move, …
Cell editors Column cell render Display shell; heavy editor only while editing

The smallest useful grid: text cells + clipboard + fill.

Open in
Paste a spreadsheet (Ctrl/Cmd+V) to fill rows
TaskAssigneeStatus
Preview with Controlled State
Open in
Paste a spreadsheet (Ctrl/Cmd+V) to fill rows
TaskAssigneeStatus
Current Grid State
Live view of the grid engine state for demonstration
Focused Cell:None
Editing Cell:None
Selection Anchor:None
Total Rows:3
Can Undo / Redo:No / No
Last Commit:None
  1. Create the engine

    const COLUMN_IDS = ["task", "assignee", "status"] as const
    const createEmptyRow = (id: string): GridRow => ({ id })
    const grid = useDataGrid<GridRow>({
    columnIds: COLUMN_IDS,
    createEmptyRow,
    initialRows,
    })
  2. Resolve raw text → CellState

    const resolveCell = (columnId: string, raw: string): CellState<string> => ({
    raw,
    value: raw || null,
    status: raw === "" ? "empty" : "valid",
    })
  3. Wire columns. accessorFn returns raw text (sort / filter / CSV for free). Wrap each cell in <DataGridCell>:

    const columns: DataTableColumnDef<GridRow>[] = COLUMN_IDS.map((id) => ({
    id,
    accessorFn: (row) => {
    const v = row[id]
    return typeof v === "object" && v != null ? v.raw : ""
    },
    header: id,
    cell: (ctx) => (
    <DataGridCell row={ctx.row.original} columnId={id}>
    {(p) => (
    <GridTextCell {...p} resolve={(raw) => resolveCell(id, raw)} />
    )}
    </DataGridCell>
    ),
    }))
  4. Compose

    <DataTableRoot data={grid.rows} columns={columns} getRowId={(r) => r.id}>
    <DataGrid grid={grid}>
    <DataGridClipboard resolveCell={resolveCell} />
    <DataGridFillHandle />
    <DataTable maxHeight={360}>
    <DataTableVirtualizedHeader />
    <DataTableVirtualizedBody estimateSize={37}>
    <DataTableRowContextMenuSlot>
    <GridRowMenu />
    </DataTableRowContextMenuSlot>
    </DataTableVirtualizedBody>
    </DataTable>
    </DataGrid>
    </DataTableRoot>

Drop any of these inside <DataGrid>. Unmounted = zero cost.

Component Capability
DataGridClipboard Copy / cut / paste (quote-aware TSV)
DataGridFillHandle Corner-drag fill (Ctrl/Cmd+Enter fill-selection works grid-wide, no handle needed)
DataGridMove Drag selection border to move (Ctrl/Cmd to copy)
DataGridRowReorder Drag rows by grip (avoid combining with sort)
DataGridCrossHighlight Row / column chrome for the selection
DataGridStatusBar Count / Sum / Avg / Min / Max
DataTableColumnResize Drag resize; double-click to autosize
DataGridUndo / DataGridRedo History

Filters, sort, pin, and the view menu are regular Niko Table components. Nest them under DataTableRoot (beside or inside <DataGrid>). Do not put DataGridUndo / DataGridAddRows / clipboard outside <DataGrid>.

Layered lists: Components, Core, Hooks. Full dump: API Reference.

Arrows, Tab, Enter, Escape navigate and edit. Type to start editing. Mod+A select all; Mod+Arrow jump to edge; Delete clears; Mod+C / X / V clipboard; Mod+Z / Mod+Shift+Z undo/redo. DataGridShortcutsButton opens the cheat sheet.

useDataGrid owns the rows (uncontrolled, like defaultValue). Controlling every keystroke would re-derive the table model and break undo. Persist with:

  • Save button: read grid.rows (or useGridChanges().getChangeSet()) on submit
  • Autosave: debounce the network, not the UI

See Persistence.