Editing & selection
Cell selection & clipboard
Select ranges of cells, copy and paste them as spreadsheet text, fill with a drag handle, and see live totals for a selection or the whole grid.
Cells select like a spreadsheet: click a cell, then drag or Shift-click to select a rectangle. A selection copies as tab-separated text that pastes into a spreadsheet, and text from a spreadsheet pastes back into editable cells. A footer shows live statistics for the selected numbers.
Drag across Price and Qty to see the sum and average below the grid, copy with Ctrl/⌘+C, or drag the small square at the selection’s corner down to fill. The pinned last row totals every column.
Selecting
- Click a cell; drag or Shift-click to extend to a rectangle.
- Shift+arrows extend from the keyboard, and Ctrl/⌘+A selects everything.
- Escape clears the selection.
The full key map is in Keyboard & hotkeys.
Copy and paste
Ctrl/⌘+C copies the selection as tab-separated text, which a spreadsheet pastes as cells. Ctrl/⌘+V pastes such a block into editable cells, starting at the selection's top-left corner:- A single copied value fills the whole selection.
- A block is cut off at the grid’s edges.
- Values go through the same checks as typing: cells in columns that are not editable, and values that
parseorvalidaterefuse, are skipped. - Every accepted cell calls
onCellEdit, so paste does nothing until you handle that callback. See Inline editing.
Fill handle
fillHandle puts a small square on the selection’s bottom-right corner. Drag it down or right to copy the selected values over the cells you drag across, as a spreadsheet does; a multi-cell selection repeats as a pattern. Only editable columns are filled, in-memory data only.
<Grid {rows} {columns} height={480} fillHandle onCellEdit={(e) => (e.row[e.column.key] = e.value)} /> Edits, pastes and fills undo with Ctrl/⌘+Z and redo with Ctrl/⌘+Y; a paste or a fill undoes as one step.
Totals for a selection
When more than one cell is selected, a bar under the grid shows Sum, Avg, Count, Min and Max of the numbers in the range, and keeps them current as a live feed ticks. aggregations picks which to show:
<Grid {rows} {columns} height={480} aggregations={['sum', 'avg', 'count']} /> A totals row
footer pins a totals row to the bottom: every column with a groupAgg shows that aggregate over all rows that pass the filters. In-memory data only.
const columns: ColumnDef[] = [
{ type: 'number', key: 'qty', header: 'Qty', groupAgg: 'sum' },
{ type: 'price', key: 'price', header: 'Price', groupAgg: 'avg' },
]; <Grid {rows} {columns} height={480} footer /> The totals follow a live feed without rescanning every row: api.patchRows moves them by the rows it changed, and rows changed any other way ($state rows, an onCellEdit handler) update them too. A computed column must depend only on its own row for this; one that reads other rows or the clock gets a fresh total on the next view or api.refresh().
Read-only grids and an active row
For a display grid, cellSelection={false} drops the selection highlight and the fill handle, so clicks go straight to onRowClick and onCellClick:
<Grid {rows} {columns} height={480} cellSelection={false} onRowClick={(row) => open(row)} /> For a list beside a detail view, drive selectedRowId (by getRowId) to highlight the active row. It is separate from the cell selection and from checkbox row selection:
<Grid {rows} {columns} height={480} selectedRowId={active?.id} onRowClick={(row) => (active = row)} /> Pinned rows
Rows can stay above the scroll, whether rows of your own (pinnedRows) or rows readers pin from the row menu (rowPinning): see Pinned rows.
Reference
cellSelection boolean Default trueHighlight cells/ranges on click-drag (the blue selection fill + focus ring + fill handle). Set false for a read-only/display grid where clicks should pass straight through to onRowClick/onCellClick without a selection highlight.
fillHandle boolean Default falseShow an Excel-style fill handle at the selection's bottom-right corner; drag it to copy the selected value(s) across the extended range (editable columns only). In-memory mode only.
aggregations AggKind[] Default ['sum', 'avg', 'count', 'min', 'max']Which stats the selection footer shows over a multi-cell range.
Example
<Grid {rows} {columns} height={480} aggregations={['sum', 'count']} />footer boolean Default falseShow a pinned totals row: each column with a groupAgg shows that aggregate over all (filtered) rows. In-memory mode only.
selectedRowId string | number | null Default nullControlled "active row" highlight (keyed by getRowId) — for master-detail / list-detail where one row is selected. Independent of the checkbox rowSelection and the cell range selection. null = none.
onRowClick (row: GridRow, event: MouseEvent | KeyboardEvent) => void Called when a data row is activated by click or Enter (open a detail view, navigate, …). Edit-input and checkbox clicks are excluded.
Example
<Grid {rows} {columns} height={480} {selectedRowId} onRowClick={(row) => (selectedRowId = row.id)} />onCellClick ( info: { row: GridRow; column: ColumnDef; value: unknown }, event: MouseEvent, ) => void Called when a cell is clicked, with its row, column and value. Fires in addition to onRowClick; excluded for the edit input.
Example
<Grid {rows} {columns} height={480} onCellClick={({ row, column }) => column.key === 'sym' && openChart(row.sym)} />onCellEdit (e: CellEditEvent) => void Called when an editable cell is committed. Update your row data in here.
Example
<Grid {rows} {columns} height={480} onCellEdit={({ row, column, value }) => (row[column.key] = value)} />groupAgg AggKind Default undefinedWhen grouping is active (or with footer), show this aggregate of the column on group headers and the totals row.
Example
{ type: 'volume', key: 'qty', header: 'Qty', groupAgg: 'sum' }editable boolean Default falseAllow inline editing (double-click, Enter or F2 on the focused cell, or type to replace). Number types edit as numbers, date as a date. Display types (badge, boolean, rating, progress, tags, link, avatar, relative, sparkline, custom) edit only with an editor; a computed (value) column never does.
pinRow (key: string | number, pinned?: boolean): boolean Pin a row above the scroll (or unpin it with pinned: false), as the row menu does under rowPinning; pinned is true unless given. The row stays in the body too. Returns false when no row has that id.
Example
api.pinRow(row.id);getPinnedRowIds (): (string | number)[] Ids of the rows pinned at runtime, in pin order.
Example
for (const id of api.getPinnedRowIds()) api.pinRow(id, false);