Row Selection & Actions
A checkbox gutter with a tri-state select-all header, shift-click range select, and a configurable bulk-actions dropdown.
Installation
Row selection ships with Data Table — no separate install.
npx shadcn@latest add @kotsas-ui/data-tableThis feature confirms its results with toasts. The install adds components/ui/sonner.tsx for you, but the <Toaster /> it exports must be mounted once in your root layout — otherwise the table works but its feedback never appears.
import { Toaster } from "@/components/ui/sonner"
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Toaster />
</body>
</html>
)
}Usage
Select rows (try shift-click for a range), then open Actions — each action receives the selection via onClick({ rowIds, rows, allMatching }). Selected rows also scope footer aggregation and Delete/Backspace bulk-clear.
1 | Set up CI pipeline | High | 12 | $2,400.00 |
2 | Design onboarding flow | Medium | 8 | $3,200.00 |
3 | Migrate auth to OAuth2 | Urgent | 21 | $5,600.00 |
4 | Write API docs | Low | 0 | $900.00 |
5 | Fix pagination bug | High | 4 | $600.00 |
6 | Upgrade Next.js to 16 | Medium | 16 | $1,800.00 |
Examples
With server-driven pagination, the select-all header cycles to a third state — every row matching the current filter, loaded or not:
// With server-driven pagination, the select-all header gains a third state:
// none → all loaded → ALL MATCHING (rows not yet loaded included). Your
// actions receive the loaded rows; treat isAllMatchingSelected server-side.
<DataTable
data={loadedPage}
columns={columns}
getRowId={(row) => row.id}
enableRowSelection
manualPagination
totalRowCount={8412}
/>API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| enableRowSelection? | boolean | false | Adds the row-number/checkbox gutter column with tri-state select-all and Sheets/Gmail-style shift-click range select. |
| actions? | DataTableAction[] | — | Bulk actions shown in the Actions dropdown next to Columns. The trigger is disabled while nothing is selected. |
| DataTableAction.onClick | ({ rowIds, rows, allMatching }) => void | — | Receives the selected rows; the popover closes after the click. allMatching is true when the select-all cycle reached "all matching rows" under manual pagination — rows then covers only the loaded subset, so run a server-side bulk path for the full scope. |
| DataTableAction.icon? | ComponentType | — | Optional leading icon (any lucide icon works). |
| DataTableAction.variant? | "default" | "destructive" | "default" | Destructive actions render in the destructive color. |
| DataTableAction.disabled? | boolean | false | Disables one action row without hiding it. |
| getRowId? | (row, index) => string | index | Stable row identity — required for selection to survive sorting and filtering. |