gridcn1.3.0

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.

columns.ts
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.

people-grid.tsx
"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.

Name
Age
Active
Ada Lovelace
28
Grace Hopper
34
Margaret Hamilton
31

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.

people-grid.tsx
"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.

What's next

On this page