
Description
Select rectangular cell ranges with cellSelectionFeature: two-corner range state keyed by row and column id, mousedown/mouseenter handlers, selection edges, render-order resolution under pinning, and autoResetCellSelection. Load when ranges widen unexpectedly after sorting or column reordering, when a drag re-renders the whole table, or when building copy-to-clipboard from a selection.
SKILL.md
This skill builds on core and table-features. cellSelection is an array of rectangles, each stored as two corner cells identified by row and column id. It is not a per-cell map, and it is not positional.
Setup
import { cellSelectionFeature, tableFeatures } from '@tanstack/table-core'
type Person = { id: string; name: string }
export const features = tableFeatures({ cellSelectionFeature })
export const options = {
getRowId: (row: Person) => row.id,
}
State shape:
type CellSelectionRange = {
anchorRowId: string
anchorColumnId: string
focusRowId: string
focusColumnId: string
}
type CellSelectionState = Array<CellSelectionRange>
The anchor corner stays put; the focus corner moves during a drag or Shift-extend. Two corners are what make Shift-extend possible, and they keep a drag across thousands of cells to a two-field write.
Core Patterns
Bind both mouse handlers
const onMouseDown = cell.getSelectionStartHandler()
const onMouseEnter = cell.getSelectionExtendHandler()
The start handler attaches its own document-level mouseup listener and removes it when the drag ends, so a pointer released outside the table still finishes correctly. Pass a document explicitly (cell.getSelectionStartHandler(iframeDocument)) only when the table renders into another document.
Read the selection
const count = table.getSelectedCellCount()
const grids = table.getSelectedCellRangesData() // [range][row][column]
Expansion APIs are memoized and pull-based, so a table that only highlights cells never pays to enumerate a large selection.
Draw the outline from edges
cell.getSelectionEdges() marks a side true when the neighbouring cell in that direction is not selected, which yields one continuous outline around a union of rectangles. All sides are false when the cell is not selected.
Drive keyboard navigation externally
The feature ships no keydown handling. Call table.moveCellSelection(direction), table.extendCellSelection(direction), table.setFocusedCell(rowId, columnId), table.selectAllCells(), and table.resetCellSelection(true) from a hotkey library such as @tanstack/react-hotkeys, scoped to the grid element rather than the document.
Common Mistakes
HIGH Expecting a per-cell selection map
Wrong: const isSelected = table.state.cellSelection[cell.id]
Correct: const isSelected = cell.getIsSelected()
cellSelection holds rectangles, not cell keys. Membership is resolved by comparing the cell's row and column index against the memoized bounds.
Source: packages/table-core/src/features/cell-selection/cellSelectionFeature.types.ts
HIGH Assuming a range is frozen to the cells it originally covered
Ranges are anchored to corner ids, so sorting, filtering, and column reordering keep the corners and recompute what sits between them. A range can therefore widen onto columns or rows the user never selected. Reset in userland when the product needs stricter behavior:
// after a column reorder or pin
table.resetCellSelection(true)
Hiding a column that a corner sits on makes the range inert rather than deleting it; it returns when the column is shown again.
Source: docs/framework/react/guide/cell-selection.md#how-ranges-survive-table-changes
HIGH Binding only mousedown and expecting drag
Wrong:
const props = { onMouseDown: cell.getSelectionStartHandler() }
Correct:
const props = {
onMouseDown: cell.getSelectionStartHandler(),
onMouseEnter: cell.getSelectionExtendHandler(),
}
Without the extend handler a drag selects only the origin cell. Do not add a mouseup binding; the start handler already owns one.
Source: examples/react/cell-selection
HIGH Deriving column position from column definition order
Wrong: const index = column.getIndex()
Correct: const isSelected = cell.getIsSelected()
Cells render start-pinned first, then center, then end. getVisibleLeafColumns() and column.getIndex() are not pinning-reordered, so indexing a selection against them makes a rectangle visually scattered as soon as a column is pinned. The feature resolves its own render-order index map; use the cell APIs rather than recomputing membership.
Source: packages/table-core/src/features/cell-selection/cellSelectionFeature.utils.ts
HIGH Re-rendering every cell on each drag update
Wrong: one subscription wrapping the whole <tbody>.
Correct: one subscription per row, with a selector returning only what changes that row's appearance.
A drag writes state on every cell boundary crossed. A table-wide subscription reconciles every cell each time. Subscribe per row against table.atoms.cellSelection and derive a key from table.getCellSelectionBounds() (memoized, so it computes once per change) covering the row itself plus the rows above and below, which decide its top and bottom edges.
Source: docs/framework/react/guide/cell-selection.md#performance-with-tablesubscribe
MEDIUM Drawing selection borders on a border-collapse table
Wrong: .cell-selected { border: 2px solid blue }
Correct: .cell-selected { box-shadow: inset 0 0 0 2px blue }
On a border-collapse table a thicker border widens the shared grid line, so rows change height as cells become selected. Box-shadow never affects layout.
Source: examples/react/cell-selection/src/index.css
MEDIUM Expecting a clipboard string from the table
Wrong: navigator.clipboard.writeText(table.getSelectedCellsAsTsv())
Correct: navigator.clipboard.writeText(toTsv(table.getSelectedCellRangesData()))
The table returns raw values only. The delimiter, the representation of null, and quoting rules are application decisions, so serialization is userland. Quote any field containing a tab, newline, or quote, or a pasted spreadsheet gains phantom columns.
Source: docs/framework/react/guide/cell-selection.md#copying-a-selection
MEDIUM Persisting a selection and expecting drag state with it
cellSelection is safe to persist because drag session state is deliberately non-reactive instance data, not part of the slice. Do not add an isSelecting field to the persisted state; a stored true would rehydrate into a drag that hovering extends and nothing ever ends.
Source: packages/table-core/src/features/cell-selection/cellSelectionFeature.types.ts
MEDIUM Fighting the automatic reset on data change
Selection resets to initialState.cellSelection whenever data changes, because new data can invalidate the row ids a range points at or silently re-select cells when ids are reused. Opt out deliberately:
export const keepAcrossDataChanges = { autoResetCellSelection: false }
autoResetAll overrides this option.
Source: packages/table-core/src/features/cell-selection/cellSelectionFeature.ts
API Discovery
Inspect node_modules/@tanstack/table-core/dist/features/cell-selection/ for CellSelectionRange, CellSelectionState, CellSelectionBounds, CellSelectionEdges, the enablement and is*Event options, and the cell and table instance APIs.
More skills from the table repository
View all 30 skillsaggregation
perform data aggregation in TanStack Table
Jul 26Data AnalysisFrontendTanStackapi-not-found
diagnose TanStack Table API errors
Jul 26DebuggingFrontendTanStackclient-vs-server
manage TanStack Table data pipelines
Jul 26Data PipelineFrontendPerformanceTanStackcolumn-faceting
build faceted filter UIs
Jul 26Data VisualizationFrontendTanStackcolumn-filtering
implement column filtering in TanStack Table
Jul 26Data AnalysisFrontendTanStackcolumn-ordering
manage TanStack Table column ordering
Jul 26FrontendTanStackUI Components
More from TanStack
View publishercolumn-pinning
configure column pinning in TanStack Table
table
Jul 26CSSFrontendTanStackUI Componentscolumn-resizing
implement column resizing in TanStack Table
table
Jul 26FrontendTanStackUI Componentscolumn-sizing
configure column sizing in TanStack Table
table
Jul 26CSSFrontendTanStackUI Componentscolumn-visibility
manage column visibility in TanStack Table
table
Jul 26FrontendTanStackUI Componentscore
build data grids with TanStack Table
table
Jul 26Data AnalysisFrontendUI Components