React Table Row Selection: Multi-Select, Single Select, and Checkbox Implementation

Row SelectionTutorialBest Practices

Master row selection in React data grids. Learn how to implement single-select, multi-select with checkboxes, keyboard navigation, shift-click ranges, select-all, and programmatic selection with production-ready code examples.

Row selection is one of the most common features in data grids—letting users select rows to perform bulk actions like delete, export, or edit. But implementing it properly with checkboxes, keyboard accessibility, and proper state management can be tricky.

This comprehensive guide covers everything you need to know about row selection in React tables:

  • Multi-select with checkboxes: Select multiple rows with checkboxes
  • Select-all functionality: Master checkbox with indeterminate state
  • Keyboard accessibility: Tab navigation and Space/Enter for selection
  • State management: Efficient Set-based selection tracking
  • Bulk operations: Working with selected rows for actions

We'll compare Simple Table's batteries-included implementation with manual approaches. If you're comparing table libraries, check out our guide to the top React table libraries for 2025.

What is Row Selection?

Row selection allows users to select multiple rows using checkboxes for bulk operations. It's the standard pattern you see in email clients, admin panels, and data management tools.

Checkbox Selection

Checkboxes in each row let users select multiple items independently.

Common Use Cases:
  • • Bulk delete operations
  • • Export selected rows to CSV
  • • Batch status updates
  • • Mass email/notifications
Example: Gmail - select emails to archive/delete

Select-All Checkbox

Header checkbox for quickly selecting or deselecting all rows.

Checkbox States:
  • • Unchecked: No rows selected
  • • Indeterminate: Some rows selected
  • • Checked: All rows selected
Example: Admin panels - "Select all items"

Quick Implementation: Simple Table (Batteries-Included)

Simple Table provides row selection out of the box with minimal configuration. All patterns (checkboxes, keyboard navigation, select-all) work automatically. This is part of Simple Table's batteries-included approach.

Basic Row Selection Setup

Row selection in Simple Table requires just two props:

  • enableRowSelection - Enables checkboxes in rows and header
  • onRowSelectionChange - Callback that receives selection changes

📚 Complete implementation guide:

→ View Row Selection Documentation

Includes live examples, API reference, and use cases

How Selection Works

The onRowSelectionChange callback receives three parameters:

  • row - The row object that was just clicked
  • isSelected - Boolean indicating if the row was selected or deselected
  • selectedRows - Set containing all currently selected row objects

Note: Simple Table manages selection state internally. The callback is for observing changes and implementing your business logic.

Why Simple Table's Row Selection Wins

  • ✓ Just 2 props: enableRowSelection and onRowSelectionChange
  • ✓ Checkboxes in rows and header automatically rendered
  • ✓ Select-all checkbox with proper indeterminate state
  • ✓ Optimal performance with Set-based selection
  • Accessibility built-in (ARIA labels, keyboard support)
  • ✓ Selection persists across pagination, filtering, and sorting

Advanced Patterns & Use Cases

1. Bulk Actions with Selection

Build a toolbar that appears when rows are selected.

function UsersTable() {
  const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);

  const handleDelete = async () => {
    await deleteUsers(selectedRowIds);
    setSelectedRowIds([]); // Clear after action
  };

  const handleExport = () => {
    const selectedUsers = users.filter(u => selectedRowIds.includes(u.id));
    exportToCSV(selectedUsers);
  };

  const handleBulkEdit = () => {
    // Open modal with selected users
    openBulkEditModal(selectedRowIds);
  };

  return (
    <div>
      {/* Bulk actions toolbar */}
      {selectedRowIds.length > 0 && (
        <div className="mb-4 p-4 bg-blue-50 rounded flex justify-between items-center">
          <span className="font-semibold">
            {selectedRowIds.length} row{selectedRowIds.length > 1 ? 's' : ''} selected
          </span>
          <div className="space-x-2">
            <button onClick={handleExport} className="px-4 py-2 bg-blue-500 text-white rounded">
              Export
            </button>
            <button onClick={handleBulkEdit} className="px-4 py-2 bg-green-500 text-white rounded">
              Edit
            </button>
            <button onClick={handleDelete} className="px-4 py-2 bg-red-500 text-white rounded">
              Delete
            </button>
          </div>
        </div>
      )}

      <SimpleTable
        rows={users}
        defaultHeaders={headers}
        rowIdAccessor="id"
        rowSelection={{
          enabled: true,
          selectedRowIds,
          onSelectedRowsChange: setSelectedRowIds,
        }}
        height={400}
      />
    </div>
  );
}

4. Select-All with Large Datasets (Confirmation Pattern)

When dealing with thousands of rows, confirm before selecting all.

function UsersTable({ totalUsers }: { totalUsers: number }) {
  const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
  const [showSelectAllConfirm, setShowSelectAllConfirm] = useState(false);

  const handleSelectAll = () => {
    if (totalUsers > 100) {
      // Show confirmation for large datasets
      setShowSelectAllConfirm(true);
    } else {
      // Select all immediately
      const allIds = users.map(u => u.id);
      setSelectedRowIds(allIds);
    }
  };

  return (
    <div>
      {/* Confirmation banner */}
      {showSelectAllConfirm && (
        <div className="mb-4 p-4 bg-amber-50 border border-amber-300 rounded">
          <p>
            You are about to select <strong>{totalUsers} rows</strong>. This may affect
            performance. Continue?
          </p>
          <button
            onClick={() => {
              const allIds = users.map(u => u.id);
              setSelectedRowIds(allIds);
              setShowSelectAllConfirm(false);
            }}
            className="mt-2 px-4 py-2 bg-blue-500 text-white rounded"
          >
            Yes, Select All
          </button>
          <button
            onClick={() => setShowSelectAllConfirm(false)}
            className="mt-2 ml-2 px-4 py-2 bg-gray-300 rounded"
          >
            Cancel
          </button>
        </div>
      )}

      <SimpleTable
        rows={users}
        defaultHeaders={headers}
        rowIdAccessor="id"
        rowSelection={{
          enabled: true,
          selectedRowIds,
          onSelectedRowsChange: setSelectedRowIds,
        }}
        height={400}
      />
    </div>
  );
}

Keyboard Accessibility

Row selection in Simple Table is fully accessible with keyboard support:

  • Tab/Shift+Tab: Navigate to checkboxes
  • Space or Enter: Toggle checkbox selection
  • ARIA Labels: Proper labeling for screen readers
  • Focus Management: Clear visual focus indicators

Accessibility Note

Keyboard navigation isn't just for power users—it's essential for accessibility. Screen reader users and people with motor impairments rely on keyboard support. Simple Table implements proper ARIA roles and keyboard patterns by default. Learn more in our comprehensive accessibility comparison.

Common Pitfalls When Implementing Row Selection

Using Array Instead of Set

Problem: Arrays are O(n) for lookups and can have duplicates, causing performance issues with large datasets

Solution: Use Set (O(1) lookups)

Simple Table uses Set internally for optimal performance. Convert to Array only when needed.

Missing Indeterminate State

Problem: Header checkbox doesn't show "some selected" state, confusing users

Solution: Implement Indeterminate

Simple Table handles this automatically. Manual implementations need to set the indeterminate attribute when 0 < selected < total.

Poor Keyboard Accessibility

Problem: Checkboxes can't be accessed via keyboard, screen readers don't announce selection state

Solution: Full Accessibility

Simple Table provides Tab navigation, Space/Enter for toggling, and proper ARIA labels. See accessibility comparison.

Not Persisting Selection Across Pagination

Problem: Selection clears when user navigates to another page

Solution: Stable Row Identifiers

Simple Table maintains selection across pagination, filtering, and sorting automatically. Requires proper rowIdAccessor configuration.

Manual Implementation (TanStack Table / Custom)

If you're not using Simple Table and need to implement row selection manually, be prepared for significant complexity:

Warning: Manual implementation requires handling checkbox rendering, state management, select-all logic, indeterminate state, keyboard navigation, accessibility, and edge cases. Expect 100-200+ lines of code. See our comparison of headless vs batteries-included approaches.

What You Need to Implement

  • Checkbox rendering: Individual row checkboxes and header select-all checkbox
  • State management: Track selected rows (preferably with Set for performance)
  • Select-all logic: Handle selecting/deselecting all rows
  • Indeterminate state: Show header checkbox as indeterminate when some (but not all) rows selected
  • Keyboard accessibility: Tab navigation, Space/Enter for toggling
  • ARIA labels: Proper labeling for screen readers
  • Visual feedback: Highlight selected rows
  • Integration: Work correctly with pagination, filtering, sorting

The Reality Check

A complete, production-ready manual implementation requires 200-300+ lines of code, extensive testing for edge cases, accessibility audits, and ongoing maintenance. Most teams underestimate this complexity and end up with incomplete or buggy implementations.

The Better Alternative

Simple Table handles all of this with 2 props. Save yourself weeks of development and ongoing maintenance by using a battle-tested solution. See the documentation for examples.

Best Practices: Production-Ready Row Selection

✓ Use Row IDs, Not Indexes

IDs remain stable across sorting, filtering, and pagination

✓ Provide Visual Feedback

Highlight selected rows with background color and checked checkbox

✓ Support Keyboard Navigation

Arrow keys, Space bar, Cmd/Ctrl-A for accessibility

✓ Implement Shift-Click Ranges

Power users expect spreadsheet-like range selection

✓ Show Selection Count

Display "X rows selected" for user confidence

✓ Clear Selection After Actions

Reset selection state after delete, export, or bulk edit

✓ Persist Across Pages

Keep selection when user navigates pagination

✓ Handle Edge Cases

Empty states, single-row tables, disabled rows

Conclusion: Choose the Right Approach

Row selection seems simple on the surface, but production-ready implementation requires careful attention to detail:

  • • Rendering checkboxes in each row and header
  • • Select-all checkbox with proper indeterminate state
  • • Efficient state management (Set for O(1) lookups)
  • • Keyboard accessibility (Tab, Space, Enter)
  • • ARIA labels and roles for screen readers
  • • Visual feedback for selected rows
  • • Callback handling for bulk operations
  • • Persistence across pagination, filtering, and sorting
  • • Performance optimization for large datasets

The Pragmatic Choice: Use a Library

Unless you have very specific requirements, use a library that handles row selection for you. Simple Table provides all these features out-of-the-box with just 2 props: enableRowSelection and onRowSelectionChange.

Manual implementation: 200-300+ lines of code for checkbox rendering, state management, indeterminate logic, accessibility, edge cases, and testing.
Simple Table: 2 props, everything works.

Row selection that just works—out of the box

Simple Table handles checkboxes, select-all with indeterminate state, keyboard accessibility, and ARIA labels automatically. No boilerplate, no edge cases to debug. Just set enableRowSelection and onRowSelectionChange.