react-datatable
Guides

Add row selection

Use this guide when users need to act on more than one row at a time.

When to add selection

Row selection is for work, not decoration.

Add it when users need to:

  • archive, assign, export, or update several rows at once
  • keep a working set selected while previewing or opening individual rows
  • apply a bulk workflow to either visible rows or the whole filtered result

If the table does not yet have a concrete selection-driven action, skip selection for now.

Enable selection

Selection is configured with the selection prop.

<Datatable
  tableKey="customers"
  data={rows}
  columns={columns}
  getRowId={(row) => row.id}
  selection={{
    enabled: true,
    showCheckboxOnHover: false,
  }}
/>

The current implementation is multi-select, so you do not need to set a separate mode.

When selection is enabled, the table adds a synthetic checkbox column at the start of the grid. That column is part of the interaction model, but not part of your saved user column order.

[!NOTE] Screenshot placeholder: selection column and selected rows visible in a realistic grid, with one active row that is not the same thing as the selected set.

Control which rows can be selected

Use getRowCanSelect when the next action only applies to some rows.

<Datatable
  tableKey="customers"
  data={rows}
  columns={columns}
  getRowId={(row) => row.id}
  selection={{
    enabled: true,
    getRowCanSelect: (row) => row.permissions.canArchive,
  }}
/>

Tie the predicate to the action the user is about to run.

Examples:

  • only rows the current user can archive
  • only invoices that are still unpaid
  • only records that are safe to retry

The table uses that predicate both when rendering row checkboxes and when calculating how many rows are actually selectable in the current filtered result.

Checkbox visibility

showCheckboxOnHover controls whether unselected rows keep their checkbox hidden until the row is hovered.

selection={{
  enabled: true,
  showCheckboxOnHover: true,
}}

Use hover-only checkboxes when selection is secondary to the main table workflow.

Show checkboxes all the time when:

  • bulk work is a primary use case
  • the table needs to feel obviously actionable
  • users might otherwise miss that multi-select exists

[!NOTE] Screenshot placeholder: side-by-side or annotated comparison of hover-only checkboxes versus always-visible checkboxes, showing when each selection style feels right.

Select-all scope

The selection model supports two different scopes:

  • explicit selection of concrete row IDs
  • all-matching selection for the current online query

If you only need visible or loaded-row selection, stop here. If users need actions like "export every filtered customer," enable allowSelectAllMatching.

<Datatable
  tableKey="customers"
  online={{
    mode: "pagination",
    queryKey: ["customers", "listOnline"],
    query: listCustomers,
  }}
  columns={columns}
  getRowId={(row) => row.id}
  selection={{
    enabled: true,
    allowSelectAllMatching: true,
  }}
/>

With that option enabled, the header checkbox can switch from explicit IDs to an allMatching descriptor built from the current table query.

The two selection payloads

The action layer receives a DataTableSelectionDescriptor.

type DataTableSelectionDescriptor =
  | {
      kind: "explicit"
      ids: string[]
    }
  | {
      kind: "allMatching"
      query: OnlineQueryStateInput
      includedIds: string[]
      excludedIds: string[]
      totalMatchingRows: number
    }

Use explicit when the action should target only the rows the browser has identified directly.

Use allMatching when the user means "everything matching the current filters, sorting, search, and grouping," even if many of those rows are not currently mounted.

That distinction keeps large online bulk workflows honest.

All-matching action copy

When allowSelectAllMatching is enabled, the action text must say what will actually happen.

Good examples:

  • Export all 2,400 matching customers
  • Archive 138 filtered issues
  • Assign selected rows

Bad examples:

  • Run
  • Apply
  • Export selected when the action really targets all matching rows

The point is to prevent users from thinking they selected only what they can currently see.

[!NOTE] Screenshot placeholder: all-matching selection banner or copy state such as “Export all 2,400 matching customers,” making the scope larger than the visible viewport unmistakable.

Resolve online selections on the server

For all-matching selections, do not expect the browser to send every target row ID.

Use the saved query plus the exclusion list.

if (selection.kind === "explicit") {
  rows = await db.select().from(customers).where(inArray(customers.id, selection.ids))
} else {
  rows = await applyOnlineQuery(db.select().from(customers), selection.query)
  rows = rows.filter((row) => !selection.excludedIds.includes(row.id))
}

That lets the server resolve the real target set without forcing the client to load every row first.

Selection resets when the query meaningfully changes

Selection is tied to the current working result set.

When filters, search, sorting, or grouping change enough to alter that result set, the table clears selection instead of pretending the old selection still means the same thing.

A selected set from one query should not silently carry into a different query and trigger the wrong bulk action.

Verify row selection before you move on

Before you continue, confirm that:

  • selection exists because a real multi-row workflow needs it
  • active row, preview, and selection are visually distinct
  • getRowCanSelect blocks rows that should stay out of the workflow
  • header select-all behaves the right way for your product surface
  • all-matching copy states scope clearly when rows outside the viewport are included
  • server actions can interpret explicit versus allMatching correctly
  • changing the query clears stale selection instead of carrying it into a different result set

On this page