Row Selection Table
Add row selection with checkboxes for bulk actions.
Preview with Controlled State
View Full State Object
{}[]
[]
{
"pageIndex": 0,
"pageSize": 5
}{}Introduction
Section titled “Introduction”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.
Installation
Section titled “Installation”Install the DataTable core and add-ons for this example:
This example also uses checkbox from Shadcn UI for row selection:
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.
Prerequisites
Section titled “Prerequisites”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", }, // ...]Basic Table with Row Selection
Section titled “Basic Table with Row Selection”Let’s start by building a table with row selection.
Column Definitions
Section titled “Column Definitions”First, we’ll add a select column to our definitions.
"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" }, },]<DataTable /> component
Section titled “<DataTable /> component”Next, we’ll create the table with row selection enabled.
"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> )}Row Selection Column
Section titled “Row Selection Column”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,}Getting Selected Rows
Section titled “Getting Selected Rows”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> )}Selection Bar
Section titled “Selection Bar”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>Controlled Selection State
Section titled “Controlled Selection State”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> )}When to Use
Section titled “When to Use”✅ 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)
Next Steps
Section titled “Next Steps”- Advanced Table - Combine selection with filters
- Tree Table - Hierarchical selection