Data Grid
The full-parity grid — multi-key sort, column picker, grouping, load-more, virtualization, and APG cell navigation with inline editing.
- Status
- Since
0.4.0- Accessibility pattern
- APG grid
Last updated
| Globex expansion | Mel | $48,000 | ||
| Umbrella upsell | Priya | $22,000 | ||
| Northwind seats | Ada | $15,600 | ||
| Acme renewal | Priya | $12,400 | ||
| Initech pilot | Ada | $9,800 |
Install
Add Data Grid from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/data-gridThe same command installs the registry items it composes: @vegastack/auto-save-input, @vegastack/button, @vegastack/data-table-parts, @vegastack/dropdown-menu, @vegastack/editable-cell, @vegastack/table, @vegastack/use-announcer.
It also adds the sanctioned engines to your package.json: @tanstack/react-table (sorted row-model engine), @tanstack/react-virtual (row windowing/measurement engine).
Usage
import { DataGrid } from "@/components/ui/data-grid";
<DataGrid
aria-label="Deals"
columns={[
{ key: "name", header: "Name", sortable: true, mobile: "visible" },
{
key: "stage",
header: "Stage",
editable: { type: "select", options: stages },
},
{ key: "amount", header: "Amount", align: "end", mobile: "merge" },
]}
data={deals}
getRowId={(d) => d.id}
onCellCommit={(row, key, value) => api.patch(row.id, { [key]: value })}
selectable
/>;DataGrid is the commissioned full-parity sibling
DataList always pointed to. The sorted row
model comes from TanStack Table (the sanctioned engine); windowing from
TanStack Virtual behind the virtualize flag; the APG grid keyboard layer —
which no library ships — is the component's own.
Two deliberate differences from DataList, for migrators:
- Cells render as component elements, so a
renderimplementation may use hooks (DataListinvokesrenderas a plain function and cannot allow that). - The grid sorts its own data — that is what the row-model engine is for.
sort/onSortChangestay controllable for URL state.
Scope
| Behaviour | Where it lives |
|---|---|
| Data fetching, filters, saved views | Host (G7) — FilterBuilder + the toolbar slot |
| The cell write + conflict handling | Host — onCellCommit is a request; cellStatus is your word |
| Pagination UI | The footer slot; loadMore covers the continuous case |
| Simple presentational tables | DataList — still the right default |
| The shared table chrome | data-table-parts — sort header, selection, async states |
The toolbar
The toolbar row holds the host's toolbar slot on the leading edge and the grid's own controls on
the trailing edge: the hidden-columns hint, then the "Columns" picker.
The picker is on by default and turns off with columnPicker={false} — a fixed three-column grid
should not carry a column manager, and a host that drives visibility from its own settings surface
already has one. With no toolbar, no picker and nothing hidden, the toolbar row is not rendered at
all.
Examples
Grouping
One group column renders a collapsible <tbody> section per value — valid
HTML where a div-based collapsible between tbody and tr is not. Grouping
and virtualize are mutually exclusive.
| Stage | ||
|---|---|---|
| Acme renewal | Qualified | $12,400 |
| Initech pilot | Qualified | $9,800 |
| Globex expansion | Proposal | $48,000 |
| Northwind seats | Proposal | $15,600 |
| Umbrella upsell | Won | $22,000 |
Inline editing
Editable columns open EditableCell in
focusMode="managed" — the grid owns reachability, the cell owns the editor
and its async status. Enter/F2 edits; Escape
restores grid navigation; a rejected commit reverts and announces.
| Name | Stage |
|---|---|
| Acme renewal | |
| Globex expansion | |
| Initech pilot | |
| Umbrella upsell | |
| Northwind seats |
Focus a cell, press Enter or F2 to edit; Escape restores grid navigation. Commits are async — watch the cell status.
Keyboard-continuous load-more
↓ past the last row fetches the next page — paging never breaks the keyboard flow.
| Name | Stage |
|---|---|
| Acme renewal | Qualified |
| Globex expansion | Proposal |
| Initech pilot | Qualified |
Responsive revelation
Columns declare a pixel budget (minWidth, default 120) and a posture for what happens when they no
longer fit. design.md § DataGrid: data is never silently lost, so the default posture is
merge:
mobile | When the column no longer fits |
|---|---|
"merge" | Default. Stacks into the primary (first) column's cell, under its main value. |
"visible" | Never hides — the column is essential and the grid scrolls instead. |
"hidden" | Drops the column, and the toolbar reports "N columns hidden". |
"hidden" is the only posture that actually removes a value, which is why it is opt-in and why it
announces itself. If you choose it, be sure the value is genuinely recoverable elsewhere.
| Deal | Stage | Amount | Owner |
|---|---|---|---|
| Acme renewal | Qualified | $12,400 | Priya |
| Globex expansion | Proposal | $48,000 | Mel |
| Initech pilot | Qualified | $9,800 | Ada |
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
columns* | DataGridColumn<T>[] | — | Column definitions, left to right (base order; see columnOrder). |
data* | T[] | — | Row data. The grid sorts it itself (the row-model engine). |
getRowId* | (row: T) => string | — | Stable, unique row id — selection and edit identity. |
aria-label | string | "Data grid" | Accessible name for the grid. |
cellStatus | ((rowId: string, key: string) => AutoSaveStatus) | — | The host's word on a cell write in flight (AutoSaveStatus). Omit to let
each cell derive status from the onCellCommit promise. |
className | string | — | Extra classes for the root. |
columnOrder | string[] | — | Column order (array of keys), applied left to right. Controlled-only: the grid ships no reorder affordance — the host owns it (a settings surface). Omit to use the declared column order. |
columnPicker | boolean | true | Render the built-in "Columns" picker in the toolbar's trailing slot. Turn it off for a grid whose columns are fixed, or when the host drives visibility from its own settings surface — a three-column read-only grid should not carry a column manager. |
columnVisibility | Record<string, boolean> | — | Controlled column visibility (key → visible). Omit for uncontrolled.
The built-in picker edits it either way. |
emptyState | React.ReactNode | — | Content when data is empty and not loading. |
footer | React.ReactNode | — | Host slot below the table. |
groupState | Record<string, "collapsed" | "expanded"> | — | Controlled collapsed state per group value. Omit for uncontrolled. |
loading | boolean | false | Show skeleton rows instead of data. |
loadMore | DataGridLoadMore | — | Keyboard-continuous load-more at the last row. |
maxHeight | string | — | Scroll-viewport max height as a CSS length — required for virtualize,
useful alone for sticky headers. Flows to the Table container through
--data-grid-max-height. |
maxSortKeys | number | 2 | Maximum simultaneous sort keys. |
onCellCommit | ((row: T, key: string, value: string) => void | Promise<void>) | — | Commit an inline cell edit. Return a promise to engage the async layer; rejection reverts and announces (EditableCell's contract). |
onColumnVisibilityChange | ((visibility: Record<string, boolean>) => void) | — | Fired when the picker (or host) changes visibility. |
onGroupStateChange | ((state: Record<string, "expanded" | "collapsed">) => void) | — | Fired when a group toggles. |
onSelectionChange | ((selectedIds: Set<string>) => void) | — | Selection change. |
onSortChange | ((sort: DataGridSort[]) => void) | — | Fired with the next sort array on header activation. |
ref | React.Ref<HTMLDivElement> | — | Ref forwarded to the root (data-slot="data-grid"). |
selectable | boolean | false | Render the leading selection column. |
selectedIds | Set<string> | — | Controlled selection. |
sort | DataGridSort[] | — | Controlled multi-sort (priority order). Omit for uncontrolled. |
toolbar | React.ReactNode | — | Host slot above the table. |
virtualize | boolean | false | Window the rows with TanStack Virtual (needs a fixed-height viewport via
maxHeight). Ignored while a group column exists. |
Data attributes and CSS variables on DataGrid
| Attribute | Values |
|---|---|
data-collapsed | "" |
data-grid-id | mirrors a prop or state value |
data-group | mirrors a prop or state value |
data-selected | "" |
data-slot | "data-grid" | "data-grid-cell" | "data-grid-footer" | "data-grid-group-row" | "data-grid-head" | "data-grid-hidden-hint" | "data-grid-load-more" | "data-grid-merged" | "data-grid-row" | "data-grid-section" | "data-grid-toolbar" | "data-grid-virtual-pad" |
--data-grid-virtual-pad | CSS custom property |
Column
| Prop | Type | Default | Description |
|---|---|---|---|
header* | React.ReactNode | — | Header label. |
key* | string | — | Stable identifier — the React key, the sort key, the visibility key. |
accessor | ((row: T) => unknown) | — | Sort/group value for the row. Defaults to reading row[key]. |
align | "center" | "end" | "start" | "start" | Horizontal alignment of the header and cells. |
editable | EditableCellEditor | — | Open this editor on Enter/F2 while the cell has grid focus. Uses
EditableCell in focusMode="managed" — the grid owns reachability, the
cell owns the editor and its async status. |
group | boolean | false | Group rows into collapsible sections by this column's value. One grouping
column at most; grouping disables virtualize. |
minWidth | number | 120 | Pixels this column needs before the responsive revelation shows it.
Columns that no longer fit hide right-to-left; mobile overrides. |
mobile | "hidden" | "merge" | "visible" | "merge" | Responsive posture when the column no longer fits: visible never hides;
merge stacks the value into the primary (first) column's cell; hidden
drops it, which is counted and reported in the toolbar so the loss is never
silent. merge is the default because design.md § DataGrid requires that
data is never silently lost. |
mono | boolean | false | Render this column's values in the mono numeral face (text-code +
tabular-nums), so figures line up down the column. |
nowrap | boolean | true for `align="end"` and `mono` columns, false otherwise | Keep this column's cells on one line instead of wrapping. Cells wrap by default (D18) — scrolling is reserved for tables that are genuinely wide, not forced by one long value. Figures and mono values are the exception and opt IN automatically. |
render | ((row: T, context: DataGridCellContext) => React.ReactNode) | — | Cell content. Rendered as a component ELEMENT (hooks are safe here —
unlike DataList.render, which is invoked as a plain function).
Ignored when the column is editable — the editor owns that cell. |
sortable | boolean | false | Header click sorts by this column; shift-click adds it as a secondary key. |
Accessibility
- Real
role="grid"semantics on a native table:columnheader/gridcellwitharia-colcount,aria-rowcount(−1 while more rows exist),aria-rowindex/aria-colindex, andaria-readonlyon non-editable cells. - One roving tab stop for the whole grid: arrows move the active cell (RTL-aware), Home/End jump within the row, Ctrl+Home/End to the grid's corners.
- Enter/F2 opens the cell editor and suspends grid navigation; Escape closes it and restores focus to the cell; edit-mode entry announces politely.
- Multi-key sort headers carry
aria-sortand a visible ordinal per key; shift-click adds a secondary key. - Responsive revelation never hides data silently:
merge(the default) stacks the value into the primary cell, and the only posture that drops a column (mobile: "hidden") reports the count in the toolbar. - The scroll viewport is keyboard-reachable exactly when it scrolls, and the roving cell's focus outline is pulled inside so the viewport cannot clip it.
| Key | Action |
|---|---|
| ↑↓←→ | Move the active cell. |
| Home / End | First / last cell in the row. |
| Ctrl+Home/End | First / last cell of the grid. |
| Enter / F2 | Edit the focused cell (suspends grid nav). |
| Escape | Close the editor, restore grid focus. |
| ↓ on the last row | Load more (when loadMore.hasMore). |
The states the verification lanes exercise for this item (generated from component-contracts.json):
| Contract | States tested |
|---|---|
| Behaviour | idle, sorted, multi-sorted, selecting, grouped, group-collapsed, editing, committing, loading, loading-more, empty, virtualized |
| Accessibility | keyboard, labeled, status-announcement, aria-grid |
| Visual | default, selected-row, sorted-header, editing-cell, loading, empty |