# Data grid

A React component in [Beamline](https://beamline.io/)'s [Data & tables](https://beamline.io/components/data) category. Live demo: https://beamline.io/components/data-grid

Virtualised grid for thousands of rows: multi-sort, search and value filters, resizable and pinnable columns, range selection, inline edit, footer aggregates, keyboard cell navigation.

## Use it for

- Any list a person works through: CRM records, orders, users, logs.
- More than ~50 rows, or when people sort, filter, select or edit.

## Not for

- A handful of static rows in a card → table.
- Trading positions with live PnL → [positions-table](https://beamline.io/components/positions-table).

## Anatomy

- toolbar (search, filter chips, selection count, actions)
- header (select-all, #, column headers with sort, pin, resize)
- virtualised rows (select, #, cells)
- footer aggregates
- loading skeleton
- empty state

## Variants

- **density**: compact 28, default 36, comfortable 44
- **toolbar**: on, off
- **rowNumbers**: on, off

## States

- loading (8 skeleton rows of the real row height, one bar per column; toolbar stays usable; the pulse stops with reduced motion)
- empty (emptyLabel centred in the body, header kept)
- error (red-tinted status band under the toolbar with a glyph, the caller's words and retry; last rows kept)
- filtered (search and value filters; controlled or not; Reset clears both and calls onFiltersChange({}) and onQueryChange(""))
- sorted (multi, with order index)
- selected (checkbox + accent tint + ink text)
- editing (editor on the field role in its focus state)
- pinned (filled pin at rest; outline pin previews on header hover or focus)
- scrolled (the last pinned column casts a logical edge shadow, also in RTL)
- unmeasured (null cells show — and read "Not measured")

## Keyboard

- role=grid, one tab stop
- Arrows move between cells, Home/End row edges, Ctrl+Home/End grid edges, PageUp/Down
- Enter or F2 edits an editable cell; typing starts an edit with that character
- Enter commits, Esc cancels, Tab commits and moves
- Space toggles the row, Shift+Space extends the range, Ctrl/⌘+A selects all visible
- Focus header with ArrowUp from the first row; Shift+F10 opens sort/pin/reset actions; resize separator arrows change 16px (Shift 64px), Home/Enter reset.

## Motion

On a sort or filter, rows that stay on screen slide to their new places (320 ms, no overshoot; instant with reduced motion). Resize and pin are immediate; hover and selection change only the row's tint.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `data` (required) | `T[]` | — |  |
| `columns` (required) | `DataGridColumn<T>[]` | — | id, header, value?, type (text\|number\|currency\|percent\|date\|tags\|status), width, pinned, sortable, filterable, editable, aggregate, format, aggregateFormat (the footer aggregate in the column's own words, e.g. 13.0 min; format needs a row), cell, tones |
| `getRowId` (required) | `(row) => string` | — |  |
| `onCellEdit` | `(rowId, columnId, value) => void` | — |  |
| `selected / defaultSelected / onSelectedChange` | `string[]` | — |  |
| `sort / defaultSort / onSortChange` | `{ id, desc }[]` | — |  |
| `filters / defaultFilters / onFiltersChange` | `Record<string, string[]>` | — | Value filters by column id: the chosen values of that column (any match keeps the row; for tags, any tag). An empty array, or no key, means no filter on that column. Controlled when `filters` is given: the chips, their Clear filter item and Reset only call onFiltersChange. A key for a column without `filterable` still filters (the caller owns it) but shows no chip; a key that names no column is ignored. |
| `query / defaultQuery / onQueryChange` | `string` | — | The toolbar search text (matches any column's value as text, case-insensitive). Controlled when `query` is given, including ""; the search field, its clear button and Reset only call onQueryChange. |
| `actions` | `(selectedIds, {hidden: string[], rowIds: string[]}) => ReactNode` | — | Exact selected IDs and current sorted/filtered view; hidden are selected IDs not in that view. |
| `density` | `'compact'\|'default'\|'comfortable'` | — |  |
| `height` | `number \| 'fill'` | `480` | The most the grid grows to before its rows scroll; a few rows take only their own height; a loading or empty grid keeps all of it, so the box does not jump when the data lands. 'fill' takes the container's height. |
| `loading / emptyLabel / rowNumbers / toolbar / footer / onRowClick` | `…` | — |  |
| `columns[].nullLabel` | `string` | — | Null cell and numeric aggregate label; defaults to Not measured. Partial numeric aggregates name their known scope and include the missing count in the cell label/title. |
| `columns[].currency` | `string` | — | ISO 4217 code for a currency column (cells and footer). Default USD. |
| `columns[].grow` | `boolean` | — | Takes the room left over when the columns are narrower than the grid, so a wide grid has no empty band at its end. Default: the widest text column (else the last column); false keeps a column at its width. |
| `selectionLabel` | `(selected: string[], hidden: string[]) => ReactNode` | — | Replaces the toolbar's “N selected · M not shown” line, e.g. to add the selection's known value; hidden selected IDs are passed so the line can stay honest. |
| `located` | `string` | — | Row ID to mark as located (aria-current, data-state located alongside selection) and scroll into view inside the grid only; for returning from a detail view to its row. |
| `onViewChange` | `(rowIds: string[]) => void` | — | The IDs in view after search, filters and sort, whenever they change; for a caller-owned view line. |

## Dependencies

`@tanstack/react-virtual`, `lucide-react`

## Import

```tsx
import { DataGrid } from "@/components/data-grid/data-grid";
```

## Get it

Data grid is part of Beamline: 190 React components and screens your coding agent (Claude Code, Codex, Cursor or any MCP client) installs into your app through Beamline's MCP server, as a ready-built package or as plain React source you can change. $49 one payment (regular $200), no subscription, a year of updates, one licence for your whole team. [Get Beamline](https://beamline.io/checkout?pack=complete) · [Connect your agent](https://beamline.io/connect)

## More in Data & tables

- [Diff table](https://beamline.io/components/diff-table): Proposed edits to a table before they happen: new rows green, removals struck through, changed cells old → new; each change can be left out and Apply…
- [Filter toolbar](https://beamline.io/components/filter-toolbar): The filters on a list kept in view: chips that remove themselves, Add filter with fields and their values, Clear all, and room for search or view controls.
- [Import mapping](https://beamline.io/components/import-mapping): Map the columns of a file to the fields of one record type, see what every value becomes, then run the import and get an outcome for each row…
- [Table](https://beamline.io/components/table): A plain, styled HTML table for small static data: header, body, footer, caption, numeric cells, three densities and a sticky header.
