Skip to main content

Selection and navigation

Cell, range, row, and column selection share one model and work with pointer, keyboard, clipboard, export, and API workflows.

Loading interactive example…
React
<Grid
rowData={rows}
columnDefs={columns}
rowNumbers
rowSelection
selectAllRowsOnHeaderClick
highlightActiveCell
onSelectionChanged={({ snapshot }) => console.log(snapshot)}
/>

Selection modes​

  • cellSelection: true enables grid focus and selection.
  • cellSelection: "text" restores native browser text selection. Both "text" and false also disable the body keyboard cursor: navigation, clipboard, and editing keys operate on the cursor, so they go with it (keyboard row selection included). The header keyboard cursor stays — disable it separately with headerKeyboardNavigation: false if the grid is deliberately inert.
  • rangeSelection enables drag and Shift-extension.
  • fillHandle (default on) puts the spreadsheet fill handle on the range's bottom-right corner; see Editing → Fill handle.
  • rowSelection uses the row-number cells for row selection.
  • rowSelection: { mode: "multiple", checkboxes: true } adds a dedicated checkbox column (independent of rowNumbers): click toggles the row — always additive, never clearing the rest — and Shift+click unions a range in. A tri-state select-all checkbox renders in its header (disable with headerCheckbox: false), covering the select-all scope (selectAllScope). Use mode: "single" to keep at most one row selected; single mode always hides the header checkbox.
  • The checkbox column starts pinned left. Set checkboxColumnPinned to "right" or null to change its initial position, and set checkboxColumnPinnable: false to lock it there. These are separate options, so any initial position can be either fixed or user-changeable.
  • columnSelection uses headers for column selection.

Arrow keys navigate one cell. Home/End, Page Up/Down, Ctrl/Cmd+Arrow block jumps, and Ctrl/Cmd+Home/End corner jumps keep the active cell visible. Ctrl/Cmd+A selects the full unified row sequence.

On the header cursor, Space selects the column, Ctrl/Cmd+Space adds one to the selection, and Shift+Arrow (or Shift+Home/Shift+End) extends the selection from the cursor's column — but only while the cursor is on a selected column, so a Shift chord never reads as plain movement that starts selecting.

The API exposes focused-cell, range, row, column, navigation, and selection snapshot operations. Pagination invalidates a range from the previous page.

// Rows
api.selectRow(3, "replace");
api.selectAllRows();
console.log(api.getSelectedRows());
api.deselectAllRows();

// Columns (columnSelection: true)
api.selectColumn("revenue", "replace");
api.selectColumn("cost", "toggle");

// Ranges
api.selectRange(2, 1); // row index 2, column index 1
api.extendRangeTo(8, 3);
console.log(api.getSelection());
api.clearSelection("range");

// Keyboard-style navigation
api.setFocusedCell(0, 0);
api.navigate("down");
api.navigate("right", { extend: true });
api.navigate("down", { jump: "block" });
api.navigate("down", { jump: "page", pageRows: 20 });
api.navigateToCorner("bottomRight");

Row selection for an external owner​

Row selection is keyed by stable row id, so an application store can drive it directly:

api.selectRowsById(["r1", "r7"]); // replace the selection
api.selectRowsById(["r9"], "add"); // extend it
api.selectRowsById(["r1"], "remove"); // shrink it
api.getSelection().selectedRowIds; // read it back

Unknown or non-selectable ids are dropped (client-side row model). Rows that are currently filtered out can still be selected by id. Programmatic changes emit selectionChanged with reason: "api"; pointer/keyboard interactions report "mouse"/"keyboard", and filter/sort-driven clears report "model". The event's delta: { added, removed } lists row-id membership changes since the previous selection event, so an external owner can update incrementally. Client-side transactions and row-data replacements automatically remove selected ids for rows that no longer exist and report that delta as a model change.

Select-all scope. api.selectAllRows(), api.areAllRowsSelected(), and the row-number header click cover the whole filtered set — all pages — by default. Set selectAllScope: "page" for page-only select-all. On the server-side row model, "filtered" covers every row the server would return, loaded or not — see Server-side select-all.

Groups select their children. By default a group row is not part of row selection: its checkbox is inert, and select-all covers the data rows only. Set groupSelectsChildren: true to make a group's checkbox stand for its subtree. Clicking it (or Space/Enter with the cursor on it) selects every selectable leaf under the group, collapsed ones included, or clears them all when all are already selected — so the first click on a partly selected group completes it. Nested groups follow, and a tree-data parent selects itself along with its descendants. The group's checkbox shows the derived state: ticked when all of its selectable rows are selected, a dash (aria-checked="mixed") when some are, empty when none are, and disabled when nothing under it is selectable. A fully selected group row paints as selected.

  • Storage stays data rows. Group rows are never stored: getSelectedRows(), getSelectedNodes() and selectedRowIds hold leaves (and tree parents) only, even for a fully selected group. Select-all, the header tri-state and areAllRowsSelected() cover those same rows.
  • What counts. Filtered-out rows never join, and rows your isRowSelectable refuses are skipped and do not count toward the group's state. A tree parent that is on screen only as the ancestor of a matching row is not selected.
  • Ranges. Shift+click unions the rows between the anchor and the clicked row, and a group row inside the range contributes its whole subtree. selectAllScope: "page" works the same way: a collapsed group on the page brings its hidden leaves with it.
  • Keyboard. In the checkbox column the cursor stops on group rows, so Space/Enter can toggle them; their other cells stay out of cell selection unless groupRowsSelectable is on.
  • Not the same as groupRowsSelectable. That option makes group rows cell-selection targets — the cursor, ranges and the clipboard — and, on its own, lets a group row be checked as a row in itself. Under groupSelectsChildren a group's checkbox always means its subtree; the two options combine.
  • Limits. Applies in mode: "multiple" (single mode ignores it) and not in pivot mode. On the server-side row model only loaded rows in the current view are affected: a collapsed group selects nothing below it until its rows are fetched. Under groupDisplayType: "groupRows" the full-width group row keeps its checkbox beside the spanning label while the checkbox column is pinned (the default, rowSelection.checkboxColumnPinned: "left"); an unpinned one sits under the label, so select those rows with a Shift range instead.

Persistence. By default a filter, sort, or quick-filter change clears the row selection. Set selectionPersistence: "keep" to retain selected row ids across model changes — selection is id-based, so it survives rows moving pages or temporarily leaving the filtered view (the cell range is still cleared, and a selectionChanged with reason: "model" still fires). Exports with scope: "selection" honor the full cross-page selection.

Server-side select-all​

The server-side row model cannot list rows it has not fetched, so select-all there is a state, not a list of ids: "every row of the current query, minus these exclusions".

api.selectAllRows(); // or the header checkbox
api.getRowSelectionState(); // { selectAll: true, toggledRowIds: [] }
// the user un-checks two rows
api.getRowSelectionState(); // { selectAll: true, toggledRowIds: ["r7", "r9"] }

// Hand the server a bulk operation: it resolves "all" against the same
// filter / quick filter it is answering rows requests with.
await archiveRows({ ...api.getRowSelectionState(), filterModel: api.getFilterModel() });
  • Per-row rule. A row is selected when selectAll is on and it is not in toggledRowIds, or when selectAll is off and it is. Rows that load later — scrolling, another page, expanding a group — paint selected without a new selectionChanged. Rows your isRowSelectable refuses stay unchecked and are never stored in toggledRowIds.
  • Gestures. A row checkbox (or Space on it) toggles the row into or out of the exclusions; Shift+click on a checkbox takes a range out of them. A gesture that replaces the selection (a plain row-number click), selectRowsById(ids) ("set"), un-checking the header and deselectAllRows() leave select-all for an explicit selection. selectRowsById(ids, "remove") adds exclusions and "add" removes them.
  • Header checkbox. Checked under select-all with nothing excluded, indeterminate with exclusions (or any explicit selection), empty otherwise. areAllRowsSelected() is true only with nothing excluded.
  • Reading ids. getSelectedRowIds(), getSelectedRows(), getSelectedNodes() and getSelection().selectedRowIds return the loaded selected rows only; getSelection() also carries selectAll: true and toggledRowIds while select-all is on (so does the selectionChanged snapshot). Exports with scope: "selection" and copying write the loaded selected rows, with a one-time console warning — run the operation on the server from getRowSelectionState() instead.
  • Restoring. api.setRowSelectionState(state) puts a saved state back. On the client-side row model (or with selectAllScope: "page", or in single mode) selectAll: true resolves at once to the scope's rows minus the exclusions — those configurations keep explicit ids, as before.
  • What ends it. A filter or quick-filter change ends select-all even under selectionPersistence: "keep" (the "all" set changed); a sort change follows selectionPersistence; refreshServerSideData() (with or without purge) keeps it, and so does paging. Replacing the data source ends it.
  • Group rows are never covered themselves. Under groupSelectsChildren a group's checkbox still acts on its loaded rows only: un-checking it excludes those rows, and its state is derived from them.