Quick start
An editable grid in a few lines, with zero state wiring.
Install
pnpm dlx shadcn@latest add @gridcn/data-grid
Check the pieces you want. The command joins them in one call. The package-manager tab (pnpm, npm, yarn, bun) is a choice: it is remembered across the whole docs site.
The CLI resolves the @gridcn registry from the hosted site
(gridcn.vercel.app). No setup step. To pin a specific tag,
branch, or commit, use the GitHub registry path:
npx shadcn add DammersCode/gridcn/data-grid#v1.0.0.
Define columns
A column maps one field of your row to one grid column. accessorKey (the accessor) names the
row field the column reads, and accessorFn computes the value instead. type names the
cell type: the built-in kind of value the column holds, such as text, number, or
checkbox. It sets how the cell renders, edits, sorts, and filters.
defineColumns<TData>() infers the value type of each column from accessorKey (or
accessorFn). It narrows options and validate through the cell type named in type.
The column id is a stable identity, never derived from the header label.
Two calls, and as const, both matter
defineColumns<TData>()([...]) is two calls: the first fixes TData, the second infers
each column from it. Add as const to the array literal too. Without both, TypeScript
widens type to string, and column-specific inference for options and validate breaks.
import { defineColumns } from "@/components/data-grid/data-grid";
export type Person = { id: string; name: string; age: number; active: boolean };
export const columns = defineColumns<Person>()([
{ id: "name", header: "Name", accessorKey: "name", type: "text", width: 160 },
{ id: "age", header: "Age", accessorKey: "age", type: "number", width: 90 },
{ id: "active", header: "Active", accessorKey: "active", type: "checkbox", width: 90 },
] as const);Render the grid
defaultData seeds the grid once. This is the uncontrolled form, the same as
<input defaultValue>: after the first render, the grid owns the array. Editing, pasting,
filling, and deleting all apply internally, and you do not need state of your own. The
controlled form passes data and takes the changes back, see Going
controlled.
"use client";
import { DataGrid } from "@/components/data-grid/data-grid";
import { columns } from "./columns";
const initialRows = [
{ id: "1", name: "Ada Lovelace", age: 28, active: true },
{ id: "2", name: "Grace Hopper", age: 34, active: true },
{ id: "3", name: "Margaret Hamilton", age: 31, active: false },
];
export function PeopleGrid() {
return (
<DataGrid
defaultData={initialRows}
columns={columns}
className="h-[400px]"
/>
);
}Every row needs a stable id
getRowId returns each row's id. It defaults to the row's id field and throws for a row
without one, so the example above omits the prop. Pass getRowId when the id lives
elsewhere. The id must be stable and unique for each row. Two rows that share an id corrupt
selection, editing, and undo and redo, because every internal lookup is row-id-based (it finds
a row by its id, not by its position). When you
change what getRowId returns, pass a new data array in the same render. In development,
the grid warns when getRowId returns other ids while data keeps its array.
Try it: click a cell and type to replace its value, double-click or press F2 to
edit in place, Enter to commit and move down (or Tab to commit in
place), drag to select a range. A click on a column header selects the whole column. It does
not sort. To sort on a header click, set headerClickBehavior="sort" (see the next step).
Add search, sorting and selection
A toolbar has to sit between the provider and the root. <DataGrid> has no slot for it, so
use the split form: DataGridProvider holds the state and the options, DataGridRoot is the
scrolling box, and DataGridHeader and DataGridBody draw the rows. Add-ons such as the
toolbar work the same way.
pnpm dlx shadcn@latest add @gridcn/data-grid-toolbar
The file below is complete. It adds quick search, a filter menu, a columns menu, sorting on header click, and a checkbox on each row. A click on a row checkbox selects that row. Press Ctrl/Cmd+Shift+Backspace to delete the selected rows.
"use client";
import {
DataGridProvider,
DataGridRoot,
DataGridHeader,
DataGridBody,
} from "@/components/data-grid/data-grid";
import {
DataGridToolbar,
DataGridSearch,
DataGridFilterMenu,
DataGridColumnsMenu,
} from "@/components/data-grid-toolbar/data-grid-toolbar";
import { columns } from "./columns";
const initialRows = [
{ id: "1", name: "Ada Lovelace", age: 28, active: true },
{ id: "2", name: "Grace Hopper", age: 34, active: true },
{ id: "3", name: "Margaret Hamilton", age: 31, active: false },
];
export function PeopleGrid() {
return (
<DataGridProvider
defaultData={initialRows}
columns={columns}
headerClickBehavior="sort"
rowMarkers="checkbox"
>
<DataGridToolbar>
<DataGridSearch />
<DataGridFilterMenu />
<DataGridColumnsMenu />
</DataGridToolbar>
<DataGridRoot className="h-[400px]">
<DataGridHeader />
<DataGridBody />
</DataGridRoot>
</DataGridProvider>
);
}A click on a header cycles that column through ascending, descending, and no sort. Shift-click adds a second sort column. Search highlights matches and jumps to them. It never hides rows. A filter hides rows. See Sorting, filtering & search and Row operations.
defaultData is the uncontrolled option, the same pattern as <input defaultValue>. When you
need your own source of truth, for example a server round-trip, an undo stack, or derived data,
pass data and onDataChange instead. See Going controlled below, or the
full picture in the three shapes table of
Events & state and the
API reference.
Going controlled
Pass data instead of defaultData when you need your own state, for example a server
round-trip, a store, or derived data. data and defaultData are mutually exclusive. If you
give both, data wins.
const [data, setData] = useState<readonly Person[]>(initialRows);
<DataGrid
data={data}
columns={columns}
getRowId={(row) => row.id}
onDataChange={(next) => setData(next)}
/>;The current data is the data you hold. After each change, call onDataChange(next) and store
next. The grid reads the array you pass back in.
onDataChange fires once per user gesture (one edit, one paste, one fill, or one delete-range).
It passes the next array and a row-id-based operations batch. See Recipes for how to use
the operations batch directly, for example to sync to a server. onDataChange also fires in
uncontrolled (defaultData) mode, as an optional notification. The grid applies the change
internally either way.
Undo & redo
For undo and redo, install the data-grid-history add-on and swap defaultData for its
useDataGridState hook. This adds one line, and still needs no manual state wiring.
pnpm dlx shadcn@latest add @gridcn/data-grid-history
import { useDataGridState } from "@/components/data-grid-history/data-grid-history";
const grid = useDataGridState(initialRows, { getRowId: (row) => row.id });
<DataGrid {...grid} columns={columns} />;Press Ctrl/Cmd+Z to undo, and Ctrl/Cmd+Y to redo. See Undo & redo for the full add-on: a composable history hook, capped stacks, and toolbar wiring.
<DataGrid> is shorthand
<DataGrid> is a convenience wrapper around four pieces: DataGridProvider, DataGridRoot,
DataGridHeader, and DataGridBody. Every add-on on this site composes those four pieces
directly, because a toolbar or a context menu needs to sit between them. See
the API reference for the expanded form and what each piece owns.