Row selection and bulk actions
Selection is on by default; col.select() gives it a checkbox column. Tick rows, shift-click a
range, tick the header for the whole page, and a bar appears with the count and your actions.
| Yuki Menon | Data | Paused | $48.00 | Apr 20, 2024 | |
| Kwame Patel | Support | Active | $1,896.00 | Aug 15, 2023 | |
| Freya Menon | Research | Churned | $0.00 | Oct 5, 2025 | |
| Meera Nguyen | Growth | Churned | $0.00 | Feb 2, 2024 | |
| Vihaan Müller | Data | Churned | $0.00 | Sep 6, 2023 | |
| Mateo Nguyen | Research | Active | $24.00 | Nov 28, 2025 | |
| Omar Tanaka | Research | Churned | $0.00 | Nov 3, 2025 | |
| Arjun Kim | Finance | Active | $2,923.00 | Apr 26, 2023 | |
| Vihaan Reddy | Platform | Active | $0.00 | Dec 9, 2024 | |
| Amelia Smith | Sales | Active | $4,740.00 | Mar 12, 2026 |
import * as React from "react";import { PauseIcon, Trash2Icon } from "lucide-react";
import { Button } from "@/components/ui/button";import { DataGrid, createDataGridColumns, useDataGrid } from "@/components/ui/data-grid";
import { makePeople, STATUS_OPTIONS, type Person } from "@/lib/data-grid-demo-data";
const col = createDataGridColumns<Person>();
const columns = col.columns([ col.select(), col.text("name", { header: "Name", size: 180 }), col.text("team", { header: "Team", size: 120 }), col.enum("status", { header: "Status", options: STATUS_OPTIONS, size: 120 }), col.currency("mrr", { header: "MRR", currency: "USD", size: 120 }), col.date("joinedAt", { header: "Joined", size: 130 }),]);
/** Click a checkbox, shift-click for a range, tick the header for the page. Churned rows can't be selected. */export default function DataGridSelection() { const [data, setData] = React.useState(() => makePeople(60, 16)); const grid = useDataGrid<Person>({ name: "selection", data, columns, getRowId: (row) => row.id, selection: { enable: (row) => row.status !== "churned" }, });
// Selection is keyed by row id, so clear it once the rows it points at have changed. const pause = (rows: Person[]) => { const ids = new Set(rows.map((r) => r.id)); setData((prev) => prev.map((p) => (ids.has(p.id) ? { ...p, status: "paused", active: false } : p)), ); grid.table.resetRowSelection(true); }; const remove = (rows: Person[]) => { const ids = new Set(rows.map((r) => r.id)); setData((prev) => prev.filter((p) => !ids.has(p.id))); grid.table.resetRowSelection(true); };
return ( <DataGrid grid={grid} bulkActions={(rows) => ( <> <Button variant="outline" size="sm" onClick={() => pause(rows)}> <PauseIcon /> Pause {rows.length} </Button> <Button variant="destructive" size="sm" onClick={() => remove(rows)}> <Trash2Icon /> Delete </Button> </> )} /> );}Use it
Section titled “Use it”Put col.select() first in the columns, and give the grid getRowId so the selection follows the
row rather than its position:
const columns = col.columns([ col.select(), col.text("name", { header: "Name" }), // …])
<DataGrid data={customers} columns={columns} getRowId={(c) => c.id} bulkActions={(rows, grid) => ( <> <Button size="sm" variant="outline" onClick={() => exportRows(rows)}>Export {rows.length}</Button> <Button size="sm" variant="destructive" onClick={() => archive(rows)}>Archive</Button> </> )}/>bulkActions receives the selected rows as your objects and the grid api, and whatever it returns
is rendered in the bar while anything is selected.
Selecting
Section titled “Selecting”| Do this | To |
|---|---|
| Click a checkbox | Toggle that row. |
| Shift + click a checkbox | Select or deselect every row between it and the last one you clicked. |
| Tick the header checkbox | Select every row on the current page; tick again to clear them. It shows a dash while only some of the page is selected. |
| Space on a focused cell | Toggle the row (with its sub-rows). Needs cell selection, which is on by default. |
| Click a row | Toggle it, but only when there is no checkbox column and cell selection is off (cellSelection={false}), so plain clicks have no other meaning. |
The header selects the page, not every row in the result. On a grid with thousands of rows
that keeps “select all” meaning the rows you can see, so a bulk action never runs on rows you never
looked at. To select the whole result from your own control, call
grid.table.toggleAllRowsSelected(true).
Single selection
Section titled “Single selection”<DataGrid selection={{ mode: "single" }} />Selecting a row replaces the previous one; the header checkbox is not rendered.
Which rows can be selected
Section titled “Which rows can be selected”<DataGrid selection={{ enable: (row) => row.status !== "churned" }} />A row that fails the test renders no checkbox, is skipped by the header checkbox and ranges, and ignores Space.
Tree data
Section titled “Tree data”With getSubRows, selecting a parent selects its children, and a parent with some children
selected shows a dash. selection={{ subRows: false }} makes each row independent. See
expanding and tree data.
Reading and changing the selection
Section titled “Reading and changing the selection”grid.table.state.rowSelection // { [rowId]: true }grid.table.getSelectedRowModel().rows // the selected TanStack rows (row.original is yours)grid.table.getFilteredSelectedRowModel().rows // only those that pass the current filtersgrid.table.resetRowSelection(true) // cleargrid.table.getRow(id).toggleSelected(true) // select oneHow it behaves
Section titled “How it behaves”- Selection is keyed by row id and survives sorting, filtering and paging: the count in the pager (“x of y row(s) selected.”) and the bar include rows on other pages.
- The bar,
DataGridBulkActions, shows the count, a Clear selection button and your actions. On<DataGrid>it sits between the toolbar and the table; when composing, render it where you like insideDataGridRootand pass your actions as its children function. - The count is the number of selected rows that exist: when your action removes rows from
data, the bar and the pager drop them at once. Callgrid.table.resetRowSelection(true)afterwards anyway if the ids should not linger in the state (they would re-select rows that come back with the same id). - The export menu offers Selected rows only while rows are selected, and
grid.exportCsv(name, { selectedOnly: true })does the same in code. - Selected rows get
data-state="selected"andaria-selected, and the grid setsaria-multiselectableon itsrole="grid"element. Style them with[data-state=selected]or throughgetRowClassName. selection={false}turns the whole thing off: no checkboxes, no bar, no count in the pager, and theselectcolumn, if you left it in, renders empty.
Keyboard
Section titled “Keyboard”| Key | What it does |
|---|---|
| Space | Toggle the focused row |
| Shift + click | Range from the last click |
Options
Section titled “Options”| Option | Type | Default | What it does |
|---|---|---|---|
selection |
boolean or object | true |
Row selection, or its settings: { mode?, enable?, range?, subRows? }. |
selection.mode |
string | "multiple" |
"multiple" or "single": many rows, or one at a time. |
selection.enable |
function | every row | (row) => boolean: which rows can be selected. |
selection.range |
boolean | true |
Shift-click selects a range. |
selection.subRows |
boolean | true |
Selecting a parent selects its children. |
bulkActions |
function | (rows, grid) => ReactNode: your controls in the bar, on <DataGrid>. |
|
initialState.rowSelection |
object | {} |
{ [rowId]: true }: start with rows selected. |
getRowId |
function | the index | (row, index, parent) => string: stable ids. Without it, selection sticks to positions. |
| Labels | strings | English | selectAll, selectRow, selected, clearSelection and selectedOf in labels; see internationalisation. |
See also
Section titled “See also”- Row actions: a menu for one row at a time.
- Cell selection and clipboard: selecting cells rather than rows.
- Export: the selected rows as CSV, JSON or clipboard text.