Skip to content

Row Selection Table

Add row selection with checkboxes for bulk actions.

Open in
Preview with Controlled State
Open in

The Row Selection Table adds checkboxes to each row, allowing users to select individual or multiple rows for bulk actions like delete, export, or update.

  1. Add the required components:
npx shadcn@latest add table input button dropdown-menu checkbox tooltip
  1. Add tanstack/react-table dependency:
npm install @tanstack/react-table
  1. Copy the DataTable components into your project. See the Installation Guide for detailed instructions.

We are going to build a table to show customers with row selection. Here’s what our data looks like:

type Customer = {
id: string
name: string
email: string
company: string
phone: string
}
const data: Customer[] = [
{
id: "1",
name: "John Doe",
company: "Acme Corp",
phone: "555-0100",
},
// ...
]

Let’s start by building a table with row selection.

First, we’ll add a select column to our definitions.

columns.tsx
"use client"
import {
DataTableColumnHeader,
DataTableColumnTitle,
DataTableColumnSortMenu,
} from "@/components/niko-table/components"
import type { DataTableColumnDef } from "@/components/niko-table/types"
import { Checkbox } from "@/components/ui/checkbox"
export type Customer = {
id: string
name: string
email: string
company: string
phone: string
}
export const columns: DataTableColumnDef<Customer>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={value => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={value => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "name",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: { label: "Name" },
},
{
accessorKey: "email",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: { label: "Email" },
},
{
accessorKey: "company",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: { label: "Company" },
},
{
accessorKey: "phone",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: { label: "Phone" },
},
]

Next, we’ll create the table with row selection enabled.

row-selection-table.tsx
"use client"
import { useState, useMemo } from "react"
import {
DataTableRoot,
DataTable,
DataTableHeader,
DataTableBody,
DataTableEmptyBody,
} from "@/components/niko-table/core"
import {
DataTableToolbarSection,
DataTablePagination,
DataTableSearchFilter,
DataTableViewMenu,
DataTableColumnHeader,
DataTableColumnTitle,
DataTableColumnSortMenu,
} from "@/components/niko-table/components"
import type { DataTableColumnDef } from "@/components/niko-table/types"
import { Checkbox } from "@/components/ui/checkbox"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { Trash2, X } from "lucide-react"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
type Customer = {
id: string
name: string
email: string
company: string
phone: string
}
const columns: DataTableColumnDef<Customer>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={value => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={value => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "name",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: { label: "Name" },
},
{
accessorKey: "email",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: { label: "Email" },
},
{
accessorKey: "company",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: { label: "Company" },
},
{
accessorKey: "phone",
header: () => (
<DataTableColumnHeader>
<DataTableColumnTitle />
<DataTableColumnSortMenu />
</DataTableColumnHeader>
),
meta: { label: "Phone" },
},
]
export function RowSelectionTable({ data }: { data: Customer[] }) {
const [rowSelection, setRowSelection] = useState({})
// Get selected rows
const selectedRows = useMemo(() => {
return Object.keys(rowSelection)
.filter(key => rowSelection[key as keyof typeof rowSelection])
.map(key => data.find(row => row.id === key))
.filter(Boolean) as Customer[]
}, [rowSelection, data])
const clearSelection = () => {
setRowSelection({})
}
return (
<DataTableRoot
data={data}
columns={columns}
state={{
rowSelection,
}}
onRowSelectionChange={setRowSelection}
>
<DataTableToolbarSection className="justify-between">
<div className="flex items-center gap-2">
<DataTableSearchFilter placeholder="Search customers..." />
{selectedRows.length > 0 && (
<>
<Badge variant="secondary">{selectedRows.length} selected</Badge>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={clearSelection}
className="h-8 px-2"
>
<X className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Clear selection</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
)}
</div>
<div className="flex items-center gap-2">
{selectedRows.length > 0 && (
<Button
variant="destructive"
size="sm"
onClick={() => {
console.log("Delete selected:", selectedRows)
}}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete ({selectedRows.length})
</Button>
)}
<DataTableViewMenu />
</div>
</DataTableToolbarSection>
<DataTable>
<DataTableHeader />
<DataTableBody>
<DataTableEmptyBody />
</DataTableBody>
</DataTable>
<DataTablePagination />
</DataTableRoot>
)
}

The select column is automatically detected when you use id: "select":

{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={value => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={value => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
}

Access selected rows using the table instance:

import { useDataTable } from "@/components/niko-table/core"
function BulkActions() {
const { table } = useDataTable<Customer>()
const selectedRows = table.getFilteredSelectedRowModel().rows
if (selectedRows.length === 0) return null
return (
<div className="flex items-center gap-2">
<span>{selectedRows.length} selected</span>
<Button onClick={() => handleDelete(selectedRows)}>
Delete Selected
</Button>
</div>
)
}

Use DataTableSelectionBar to show a persistent selection bar:

import { DataTableSelectionBar } from "@/components/niko-table/components"
<DataTableSelectionBar
selectedCount={selectedRows.length}
onClear={clearSelection}
>
<Button variant="destructive" size="sm" onClick={handleDelete}>
<Trash2 className="mr-2 h-4 w-4" />
Delete Selected
</Button>
</DataTableSelectionBar>

Full control over row selection:

import { useState } from "react"
import type { RowSelectionState } from "@tanstack/react-table"
export function ControlledSelectionTable({ data }: { data: Customer[] }) {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
return (
<DataTableRoot
data={data}
columns={columns}
state={{
rowSelection,
}}
onRowSelectionChange={setRowSelection}
>
{/* ... */}
</DataTableRoot>
)
}

✅ Use Row Selection Table when:

  • Users need to perform bulk actions (delete, export, update)
  • You want to show selection count
  • Multiple rows need to be selected at once
  • You need to track selected state

❌ Consider other options when:

  • You don’t need bulk actions (use Basic Table)
  • Only single selection is needed (use row click handlers)