Data grid

A React component in Beamline's Data & tables category.

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.

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

PropTypeDefaultDescription
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 / onSelectedChangestring[]—
sort / defaultSort / onSortChange{ id, desc }[]—
filters / defaultFilters / onFiltersChangeRecord<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 / onQueryChangestring—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'—
heightnumber | 'fill'480The 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[].nullLabelstring—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[].currencystring—ISO 4217 code for a currency column (cells and footer). Default USD.
columns[].growboolean—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.
locatedstring—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

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 · Connect your agent

More in Data & tables

  • 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: 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: 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: A plain, styled HTML table for small static data: header, body, footer, caption, numeric cells, three densities and a sticky header.