Docs
Basic Grid
Basic Grid
Minimal editable Data Grid with text cells, virtualization, and undo/redo.
The smallest useful Data Grid: useDataGrid + <DataGrid> + GridTextCell. You get keyboard navigation, undo/redo, and a virtualized body. Clipboard, fill, toolbar, and the row menu are opt-in children — the demo below mounts the common ones; leave out what you don’t need.
This page is the reference shape for new grid examples: see Documentation Guidelines.
Paste a spreadsheet (Ctrl/Cmd+V) to fill rows
| Task | Assignee | Status |
|---|
1"use client"2
3/**4 * niko-table/grid — the smallest useful editable grid.5 *6 * Just text cells + clipboard + fill: keyboard cell navigation, type-to-edit,7 * TSV copy/cut/paste, and the corner fill handle. Everything else (dynamic8 * columns, drag-move, row reorder, other cell types) is an opt-in child you add9 * later — see the full-featured example.10 */11import { DataTableRowContextMenuSlot } from "@/components/niko-table/components/data-table-row-context-menu-slot"12import { DataTable } from "@/components/niko-table/core/data-table"13import { DataTableColumnResize } from "@/components/niko-table/components/data-table-column-resize"14import { DataTableRoot } from "@/components/niko-table/core/data-table-root"15import {16 DataTableVirtualizedBody,17 DataTableVirtualizedHeader,18} from "@/components/niko-table/core/data-table-virtualized-structure"19import type { CellEditorProps } from "@/components/niko-table/grid/cells/cell-props"20import { GridTextCell } from "@/components/niko-table/grid/cells/grid-text-cell"21import { DataGridClipboard } from "@/components/niko-table/grid/components/grid-clipboard"22import { DataGridFillHandle } from "@/components/niko-table/grid/components/grid-fill-handle"23import { GridRowMenu } from "@/components/niko-table/grid/components/grid-row-menu"24import {25 DataGridAddRows,26 DataGridPasteHint,27 DataGridToolbar,28} from "@/components/niko-table/grid/components/grid-toolbar"29import { DataGrid } from "@/components/niko-table/grid/core/data-grid"30import { DataGridCell } from "@/components/niko-table/grid/core/data-grid-context"31import { useDataGrid } from "@/components/niko-table/grid/hooks/use-data-grid"32import type {33 CellState,34 GridRow,35} from "@/components/niko-table/grid/types/grid-cell"36import type { DataTableColumnDef } from "@/components/niko-table/types"37import * as React from "react"38
39const COLUMN_IDS = ["task", "assignee", "status"] as const40const LABELS: Record<string, string> = {41 task: "Task",42 assignee: "Assignee",43 status: "Status",44}45
46// Text cells are always valid; a resolver just wraps the raw string.47const resolveCell = (_columnId: string, raw: string): CellState<string> => ({48 raw,49 value: raw || null,50 status: raw === "" ? "empty" : "valid",51})52
53const cell = (raw: string): CellState<string> => resolveCell("", raw)54
55const INITIAL_ROWS: GridRow[] = [56 {57 id: "r1",58 task: cell("Draft proposal"),59 assignee: cell("Bailey"),60 status: cell("In progress"),61 },62 {63 id: "r2",64 task: cell("Review PR"),65 assignee: cell("Noah"),66 status: cell("Blocked"),67 },68 {69 id: "r3",70 task: cell("Ship release"),71 assignee: cell("Mia"),72 status: cell("Todo"),73 },74]75
76const createEmptyRow = (id: string): GridRow => ({ id })77
78const gridCellClassName = () =>79 "border-border border-r p-0 align-middle last:border-r-0"80
81export function GridBasic() {82 const grid = useDataGrid<GridRow>({83 columnIds: COLUMN_IDS,84 createEmptyRow,85 initialRows: INITIAL_ROWS,86 })87
88 const columns = React.useMemo<DataTableColumnDef<GridRow>[]>(89 () =>90 COLUMN_IDS.map(id => ({91 id,92 accessorFn: (row: GridRow) => {93 const v = row[id]94 return typeof v === "object" && v != null ? v.raw : ""95 },96 header: LABELS[id],97 size: 200,98 meta: { label: LABELS[id] },99 cell: ctx => (100 <DataGridCell row={ctx.row.original} columnId={id}>101 {(p: CellEditorProps) => (102 <GridTextCell103 {...p}104 resolve={raw => resolveCell(id, raw)}105 placeholder={LABELS[id]}106 />107 )}108 </DataGridCell>109 ),110 })),111 [],112 )113
114 return (115 <DataTableRoot data={grid.rows} columns={columns} getRowId={r => r.id}>116 <DataGrid grid={grid} className="space-y-2 outline-none">117 {/* Flex-fill + drag-resizable columns */}118 <DataTableColumnResize />119 <DataGridClipboard resolveCell={resolveCell} />120 <DataGridFillHandle />121 <DataGridToolbar>122 <DataGridAddRows count={3} />123 <DataGridPasteHint />124 </DataGridToolbar>125 <DataTable maxHeight={360}>126 <DataTableVirtualizedHeader />127 <DataTableVirtualizedBody<GridRow>128 estimateSize={37}129 fixedRowHeight130 getCellClassName={gridCellClassName}131 >132 <DataTableRowContextMenuSlot>133 <GridRowMenu />134 </DataTableRowContextMenuSlot>135 </DataTableVirtualizedBody>136 </DataTable>137 </DataGrid>138 </DataTableRoot>139 )140}Preview with Controlled State
Paste a spreadsheet (Ctrl/Cmd+V) to fill rows
| Task | Assignee | Status |
|---|
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"use client"2
3/**4 * niko-table/grid — basic editable grid with a live state panel for docs.5 */6import { DataTableRowContextMenuSlot } from "@/components/niko-table/components/data-table-row-context-menu-slot"7import { DataTable } from "@/components/niko-table/core/data-table"8import { DataTableColumnResize } from "@/components/niko-table/components/data-table-column-resize"9import { DataTableRoot } from "@/components/niko-table/core/data-table-root"10import {11 DataTableVirtualizedBody,12 DataTableVirtualizedHeader,13} from "@/components/niko-table/core/data-table-virtualized-structure"14import type { CellEditorProps } from "@/components/niko-table/grid/cells/cell-props"15import { GridTextCell } from "@/components/niko-table/grid/cells/grid-text-cell"16import { DataGridClipboard } from "@/components/niko-table/grid/components/grid-clipboard"17import { DataGridFillHandle } from "@/components/niko-table/grid/components/grid-fill-handle"18import { GridRowMenu } from "@/components/niko-table/grid/components/grid-row-menu"19import {20 DataGridAddRows,21 DataGridPasteHint,22 DataGridToolbar,23} from "@/components/niko-table/grid/components/grid-toolbar"24import { DataGrid } from "@/components/niko-table/grid/core/data-grid"25import { DataGridCell } from "@/components/niko-table/grid/core/data-grid-context"26import { useDataGrid } from "@/components/niko-table/grid/hooks/use-data-grid"27import type {28 CellState,29 GridRow,30} from "@/components/niko-table/grid/types/grid-cell"31import type { DataTableColumnDef } from "@/components/niko-table/types"32import { Button } from "@/components/ui/button"33import {34 Card,35 CardAction,36 CardContent,37 CardDescription,38 CardHeader,39 CardTitle,40} from "@/components/ui/card"41import {42 Collapsible,43 CollapsibleContent,44 CollapsibleTrigger,45} from "@/components/ui/collapsible"46import { ChevronRight } from "lucide-react"47import * as React from "react"48
49const COLUMN_IDS = ["task", "assignee", "status"] as const50const LABELS: Record<string, string> = {51 task: "Task",52 assignee: "Assignee",53 status: "Status",54}55
56const resolveCell = (_columnId: string, raw: string): CellState<string> => ({57 raw,58 value: raw || null,59 status: raw === "" ? "empty" : "valid",60})61
62const cell = (raw: string): CellState<string> => resolveCell("", raw)63
64const INITIAL_ROWS: GridRow[] = [65 {66 id: "r1",67 task: cell("Draft proposal"),68 assignee: cell("Bailey"),69 status: cell("In progress"),70 },71 {72 id: "r2",73 task: cell("Review PR"),74 assignee: cell("Noah"),75 status: cell("Blocked"),76 },77 {78 id: "r3",79 task: cell("Ship release"),80 assignee: cell("Mia"),81 status: cell("Todo"),82 },83]84
85const createEmptyRow = (id: string): GridRow => ({ id })86
87const gridCellClassName = () =>88 "border-border border-r p-0 align-middle last:border-r-0"89
90function formatPos(91 pos: { rowId: string; columnId: string } | null | undefined,92): string {93 if (!pos) return "None"94 return `${pos.rowId} / ${pos.columnId}`95}96
97export function GridBasicState() {98 const [resetKey, setResetKey] = React.useState(0)99 return (100 <GridBasicStateInner101 key={resetKey}102 onReset={() => setResetKey(k => k + 1)}103 />104 )105}106
107function GridBasicStateInner({ onReset }: { onReset: () => void }) {108 const grid = useDataGrid<GridRow>({109 columnIds: COLUMN_IDS,110 createEmptyRow,111 initialRows: INITIAL_ROWS,112 })113
114 const columns = React.useMemo<DataTableColumnDef<GridRow>[]>(115 () =>116 COLUMN_IDS.map(id => ({117 id,118 accessorFn: (row: GridRow) => {119 const v = row[id]120 return typeof v === "object" && v != null ? v.raw : ""121 },122 header: LABELS[id],123 size: 200,124 meta: { label: LABELS[id] },125 cell: ctx => (126 <DataGridCell row={ctx.row.original} columnId={id}>127 {(p: CellEditorProps) => (128 <GridTextCell129 {...p}130 resolve={raw => resolveCell(id, raw)}131 placeholder={LABELS[id]}132 />133 )}134 </DataGridCell>135 ),136 })),137 [],138 )139
140 return (141 <div className="w-full space-y-4">142 <DataTableRoot data={grid.rows} columns={columns} getRowId={r => r.id}>143 <DataGrid grid={grid} className="space-y-2 outline-none">144 {/* Flex-fill + drag-resizable columns */}145 <DataTableColumnResize />146 <DataGridClipboard resolveCell={resolveCell} />147 <DataGridFillHandle />148 <DataGridToolbar>149 <DataGridAddRows count={3} />150 <DataGridPasteHint />151 </DataGridToolbar>152 <DataTable maxHeight={360}>153 <DataTableVirtualizedHeader />154 <DataTableVirtualizedBody<GridRow>155 estimateSize={37}156 fixedRowHeight157 getCellClassName={gridCellClassName}158 >159 <DataTableRowContextMenuSlot>160 <GridRowMenu />161 </DataTableRowContextMenuSlot>162 </DataTableVirtualizedBody>163 </DataTable>164 </DataGrid>165 </DataTableRoot>166
167 <Card>168 <CardHeader>169 <CardTitle>Current Grid State</CardTitle>170 <CardDescription>171 Live view of the grid engine state for demonstration172 </CardDescription>173 <CardAction>174 <Button variant="outline" size="sm" onClick={onReset}>175 Reset All State176 </Button>177 </CardAction>178 </CardHeader>179 <CardContent className="space-y-4">180 <div className="grid gap-2 text-xs text-muted-foreground">181 <div className="flex justify-between">182 <span className="font-medium">Focused Cell:</span>183 <span className="text-foreground">184 {formatPos(grid.focusedCell)}185 </span>186 </div>187 <div className="flex justify-between">188 <span className="font-medium">Editing Cell:</span>189 <span className="text-foreground">190 {formatPos(grid.editingCell)}191 </span>192 </div>193 <div className="flex justify-between">194 <span className="font-medium">Selection Anchor:</span>195 <span className="text-foreground">196 {formatPos(grid.selectionAnchor)}197 </span>198 </div>199 <div className="flex justify-between">200 <span className="font-medium">Total Rows:</span>201 <span className="text-foreground">{grid.rows.length}</span>202 </div>203 <div className="flex justify-between">204 <span className="font-medium">Can Undo / Redo:</span>205 <span className="text-foreground">206 {grid.canUndo ? "Yes" : "No"} / {grid.canRedo ? "Yes" : "No"}207 </span>208 </div>209 <div className="flex justify-between">210 <span className="font-medium">Last Commit:</span>211 <span className="text-foreground">212 {grid.lastCommit213 ? `${grid.lastCommit.kind} (seq ${grid.lastCommit.seq})`214 : "None"}215 </span>216 </div>217 </div>218
219 <Collapsible>220 <CollapsibleTrigger className="flex items-center gap-1 text-xs font-medium text-muted-foreground hover:text-foreground [&[data-state=open]>svg]:rotate-90">221 <ChevronRight className="size-3.5 transition-transform" />222 View Full State Object223 </CollapsibleTrigger>224 <CollapsibleContent>225 <pre className="mt-2 max-h-64 overflow-auto rounded-md bg-muted p-3 text-[10px] leading-relaxed">226 {JSON.stringify(227 {228 focusedCell: grid.focusedCell,229 editingCell: grid.editingCell,230 selectionAnchor: grid.selectionAnchor,231 canUndo: grid.canUndo,232 canRedo: grid.canRedo,233 lastCommit: grid.lastCommit,234 rowCount: grid.rows.length,235 rows: grid.rows,236 },237 null,238 2,239 )}240 </pre>241 </CollapsibleContent>242 </Collapsible>243 </CardContent>244 </Card>245 </div>246 )247}Installation
Section titled “Installation”pnpm dlx shadcn@latest add @niko-table/data-table-gridFirst time using
@niko-table? See the Installation Guide.
What’s included
Section titled “What’s included”| Piece | Role |
|---|---|
useDataGrid |
Headless rows, focus, edit lifecycle, undo/redo |
<DataGrid> |
Keyboard nav + selection around DataTable |
<DataGridCell> |
Id-addressed cell wrapper (not TanStack row.index) |
GridTextCell |
Text editor (display shell; editor mounts on edit) |
| Virtualized body | Fixed-height scroll container required |
| Opt-in children | Demo mounts clipboard, fill handle, toolbar, row menu |
Composition
Section titled “Composition”1const grid = useDataGrid({2 columnIds: ["task", "assignee", "status"],3 createEmptyRow,4 initialRows,5})6
7<DataTableRoot data={grid.rows} columns={columns} getRowId={(r) => r.id}>8 <DataGrid grid={grid}>9 <DataTable maxHeight={360}>10 <DataTableVirtualizedHeader />11 <DataTableVirtualizedBody estimateSize={37} />12 </DataTable>13 </DataGrid>14</DataTableRoot>Column cell renderers wrap editors in <DataGridCell row={…} columnId={…}>. Focus and edits stay correct after sort/filter because addressing is by row id.
- Composed Grid: clipboard, fill, filters, dynamic columns
- Cell Types · Validation
- Dynamic Columns · Persistence
- Hooks · Components: API by layer