Skip to content

Row Selection Table

Add row selection with checkboxes for bulk actions.

Adds a checkbox column wired to the table’s rowSelection state, with header-row “select all” plus the indeterminate state for partial selection. Pair it with DataTableSelectionBar to surface bulk actions — export, delete, update — only when at least one row is checked.

Open in
Name
Email
Company
Phone
John Doe
john@example.comAcme Corp555-0100
Jane Smith
jane@techco.comTechCo555-0101
Bob Johnson
bob@startup.ioStartUp Inc555-0102
Alice Williams
alice@designco.comDesignCo555-0103
Charlie Brown
charlie@consulting.comConsulting LLC555-0104
Diana Prince
diana@enterprise.comEnterprise Inc555-0105
Ethan Hunt
ethan@mission.ioMission Impossible555-0106
Fiona Green
fiona@greentech.comGreenTech555-0107
George Miller
george@media.comMedia Corp555-0108
Hannah Lee
hannah@innovation.ioInnovation Labs555-0109
Preview with Controlled State
Open in
Name
Email
Company
Phone
John Doe
john@example.comAcme Corp555-0100
Jane Smith
jane@techco.comTechCo555-0101
Bob Johnson
bob@startup.ioStartUp Inc555-0102
Alice Williams
alice@designco.comDesignCo555-0103
Charlie Brown
charlie@consulting.comConsulting LLC555-0104
Row Selection State
Live view of the row selection table state with customer data
Search Query:None
Total Customers:10
Selected Customers:0
Unique Companies:0
Selection Percentage:0%
Sorting:None
Page:1 (Size: 5)
Hidden Columns:0
View Full State Object
Row Selection:
{}
Selected Customers:
[]
Sorting:
[]
Pagination:
{
  "pageIndex": 0,
  "pageSize": 5
}
Column Visibility:
{}

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.

Install the DataTable core and add-ons for this example:

pnpm dlx shadcn@latest add @niko-table/data-table @niko-table/data-table-selection-bar @niko-table/data-table-pagination @niko-table/data-table-search-filter @niko-table/data-table-view-menu @niko-table/data-table-column-sort

This example also uses checkbox from Shadcn UI for row selection:

pnpm dlx shadcn@latest add checkbox

First time using @niko-table? See the Installation Guide to set up the registry.

For other add-ons or manual copy-paste, see the Installation Guide.

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",
email: "john@example.com",
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 } from "@/components/niko-table/components/data-table-column-header"
import { DataTableColumnTitle } from "@/components/niko-table/components/data-table-column-title"
import { DataTableColumnSortMenu } from "@/components/niko-table/components/data-table-column-sort"
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 } from "@/components/niko-table/core/data-table-root"
import { DataTable } from "@/components/niko-table/core/data-table"
import {
DataTableHeader,
DataTableBody,
DataTableEmptyBody,
} from "@/components/niko-table/core/data-table-structure"
import { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"
import { DataTablePagination } from "@/components/niko-table/components/data-table-pagination"
import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"
import { DataTableViewMenu } from "@/components/niko-table/components/data-table-view-menu"
import { DataTableColumnHeader } from "@/components/niko-table/components/data-table-column-header"
import { DataTableColumnTitle } from "@/components/niko-table/components/data-table-column-title"
import { DataTableColumnSortMenu } from "@/components/niko-table/components/data-table-column-sort"
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/data-table-context"
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/data-table-selection-bar"
<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)