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.
- • Bulk delete operations
- • Export selected rows to CSV
- • Batch status updates
- • Mass email/notifications
Select-All Checkbox
Header checkbox for quickly selecting or deselecting all rows.
- • Unchecked: No rows selected
- • Indeterminate: Some rows selected
- • Checked: All rows selected
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 headeronRowSelectionChange- Callback that receives selection changes
📚 Complete implementation guide:
→ View Row Selection DocumentationIncludes live examples, API reference, and use cases
How Selection Works
The onRowSelectionChange callback receives three parameters:
row- The row object that was just clickedisSelected- Boolean indicating if the row was selected or deselectedselectedRows- 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.