Row Selection Table
Add row selection with checkboxes for bulk actions.
"use client"
import * as React from "react"import { DataTableRoot, DataTable, DataTableHeader, DataTableBody, DataTableEmptyBody,} from "@/components/niko-table/core"import { DataTableToolbarSection, DataTablePagination, DataTableSearchFilter, DataTableViewMenu, DataTableEmptyIcon, DataTableEmptyMessage, DataTableEmptyFilteredMessage, DataTableEmptyTitle, DataTableEmptyDescription, DataTableColumnTitle, DataTableColumnHeader, DataTableColumnSortMenu, DataTableSelectionBar,} from "@/components/niko-table/components"import { SYSTEM_COLUMN_IDS, FILTER_VARIANTS } from "@/components/niko-table/lib"import { useDataTable } from "@/components/niko-table/core"import { exportTableToCSV } from "@/components/niko-table/filters"import type { DataTableColumnDef } from "@/components/niko-table/types"import { Checkbox } from "@/components/ui/checkbox"import { Button } from "@/components/ui/button"import { Download, Trash2, UserSearch, SearchX } from "lucide-react"
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", }, { id: "2", name: "Jane Smith", company: "TechCo", phone: "555-0101", }, { id: "3", name: "Bob Johnson", company: "StartUp Inc", phone: "555-0102", }, { id: "4", name: "Alice Williams", company: "DesignCo", phone: "555-0103", }, { id: "5", name: "Charlie Brown", company: "Consulting LLC", phone: "555-0104", }, { id: "6", name: "Diana Prince", company: "Enterprise Inc", phone: "555-0105", }, { id: "7", name: "Ethan Hunt", company: "Mission Impossible", phone: "555-0106", }, { id: "8", name: "Fiona Green", company: "GreenTech", phone: "555-0107", }, { id: "9", name: "George Miller", company: "Media Corp", phone: "555-0108", }, { id: "10", name: "Hannah Lee", company: "Innovation Labs", phone: "555-0109", },]
// Selection bar component that uses the table from contextfunction SelectionBar({ selectedCount, onClear,}: { selectedCount: number onClear: () => void}) { const { table } = useDataTable<Customer>()
// Export handler const handleExport = React.useCallback(() => { exportTableToCSV(table, { filename: "selected-customers", excludeColumns: [ SYSTEM_COLUMN_IDS.SELECT, ] as unknown as (keyof Customer)[], onlySelected: true, }) }, [table])
// Delete handler const handleDelete = React.useCallback(() => { console.log("Delete selected rows") // Handle delete action here onClear() }, [onClear])
return ( <DataTableSelectionBar selectedCount={selectedCount} onClear={onClear}> <Button size="sm" variant="outline" onClick={handleExport}> <Download className="mr-2 h-4 w-4" /> Export Selected </Button> <Button size="sm" variant="destructive" onClick={handleDelete}> <Trash2 className="mr-2 h-4 w-4" /> Delete Selected </Button> </DataTableSelectionBar> )}
export default function RowSelectionExample() { const [rowSelection, setRowSelection] = React.useState({})
// Derive selected rows from rowSelection state and data const selectedRows = React.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])
// Helper to clear selection const clearSelection = React.useCallback(() => { setRowSelection({}) }, [])
const columns: DataTableColumnDef<Customer>[] = React.useMemo( () => [ { id: SYSTEM_COLUMN_IDS.SELECT, // 'id: "select"' triggers auto-detection for row selection size: 40, // Compact width for checkbox column 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", }, cell: ({ row }) => ( <div className="font-medium">{row.getValue("name")}</div> ), }, { accessorKey: "email", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> </DataTableColumnHeader> ), meta: { label: "Email", }, }, { accessorKey: "company", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> </DataTableColumnHeader> ), meta: { label: "Company", }, }, { accessorKey: "phone", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> </DataTableColumnHeader> ), meta: { label: "Phone", }, }, ], [], )
return ( <DataTableRoot data={data} columns={columns} state={{ rowSelection, }} onRowSelectionChange={setRowSelection} > <DataTableToolbarSection className="justify-between"> <DataTableSearchFilter placeholder="Search anything..." /> <DataTableViewMenu /> </DataTableToolbarSection> <SelectionBar selectedCount={selectedRows.length} onClear={clearSelection} /> <DataTable> <DataTableHeader /> <DataTableBody> <DataTableEmptyBody> <DataTableEmptyMessage> <DataTableEmptyIcon> <UserSearch className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No customers found</DataTableEmptyTitle> <DataTableEmptyDescription> There are no customers to display at this time. </DataTableEmptyDescription> </DataTableEmptyMessage> <DataTableEmptyFilteredMessage> <DataTableEmptyIcon> <SearchX className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No matches found</DataTableEmptyTitle> <DataTableEmptyDescription> Try adjusting your search to find what you're looking for. </DataTableEmptyDescription> </DataTableEmptyFilteredMessage> </DataTableEmptyBody> </DataTableBody> </DataTable> <DataTablePagination pageSizeOptions={[5, 10, 20]} /> </DataTableRoot> )}Preview with Controlled State
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:
{}"use client"
import * as React from "react"import { useState } from "react"import type { PaginationState, SortingState, ColumnFiltersState, VisibilityState, RowSelectionState,} from "@tanstack/react-table"import { DataTableRoot, DataTable, DataTableHeader, DataTableBody, DataTableEmptyBody,} from "@/components/niko-table/core"import { DataTableToolbarSection, DataTablePagination, DataTableSearchFilter, DataTableViewMenu, DataTableEmptyFilteredMessage, DataTableEmptyTitle, DataTableEmptyDescription, DataTableEmptyIcon, DataTableEmptyMessage, DataTableColumnTitle, DataTableColumnHeader, DataTableColumnSortMenu, DataTableSelectionBar,} from "@/components/niko-table/components"import { SYSTEM_COLUMN_IDS, FILTER_VARIANTS } from "@/components/niko-table/lib"import { useDataTable } from "@/components/niko-table/core"import { exportTableToCSV } from "@/components/niko-table/filters"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 { Download, Trash2, UserSearch, SearchX } from "lucide-react"import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle,} from "@/components/ui/card"
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", }, { id: "2", name: "Jane Smith", company: "TechCo", phone: "555-0101", }, { id: "3", name: "Bob Johnson", company: "StartUp Inc", phone: "555-0102", }, { id: "4", name: "Alice Williams", company: "DesignCo", phone: "555-0103", }, { id: "5", name: "Charlie Brown", company: "Consulting LLC", phone: "555-0104", }, { id: "6", name: "Diana Prince", company: "Enterprise Inc", phone: "555-0105", }, { id: "7", name: "Ethan Hunt", company: "Mission Impossible", phone: "555-0106", }, { id: "8", name: "Fiona Green", company: "GreenTech", phone: "555-0107", }, { id: "9", name: "George Miller", company: "Media Corp", phone: "555-0108", }, { id: "10", name: "Hannah Lee", company: "Innovation Labs", phone: "555-0109", },]
// Selection bar component that uses the table from contextfunction SelectionBar({ selectedCount, onClear,}: { selectedCount: number onClear: () => void}) { const { table } = useDataTable<Customer>()
// Export handler const handleExport = React.useCallback(() => { exportTableToCSV(table, { filename: "selected-customers", excludeColumns: [ SYSTEM_COLUMN_IDS.SELECT, ] as unknown as (keyof Customer)[], onlySelected: true, }) }, [table])
// Delete handler const handleDelete = React.useCallback(() => { console.log("Delete selected rows") // Handle delete action here onClear() }, [onClear])
return ( <DataTableSelectionBar selectedCount={selectedCount} onClear={onClear}> <Button size="sm" variant="outline" onClick={handleExport}> <Download className="mr-2 h-4 w-4" /> Export Selected </Button> <Button size="sm" variant="destructive" onClick={handleDelete}> <Trash2 className="mr-2 h-4 w-4" /> Delete Selected </Button> </DataTableSelectionBar> )}
export default function RowSelectionStateExample() { // Controlled state management for all table state const [globalFilter, setGlobalFilter] = useState<string | object>("") const [sorting, setSorting] = useState<SortingState>([]) const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]) const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({}) const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: 5, }) const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
// Derive selected rows from rowSelection state and data const selectedRows = React.useMemo(() => { return Object.keys(rowSelection) .filter(key => rowSelection[key]) .map(key => data.find(row => row.id === key)) .filter(Boolean) as Customer[] }, [rowSelection])
// Helper to clear selection const clearSelection = React.useCallback(() => { setRowSelection({}) }, [])
const resetAllState = () => { setGlobalFilter("") setSorting([]) setColumnFilters([]) setColumnVisibility({}) setPagination({ pageIndex: 0, pageSize: 5 }) setRowSelection({}) }
const selectAllRows = () => { const allSelected: RowSelectionState = {} data.forEach(customer => { allSelected[customer.id] = true }) setRowSelection(allSelected) }
const selectNone = () => { setRowSelection({}) }
// Calculate selection metrics const selectionMetrics = React.useMemo(() => { const selectedCompanies = Array.from( new Set(selectedRows.map(row => row.company)), )
const selectedEmails = selectedRows.map(row => row.email)
return { totalSelected: selectedRows.length, uniqueCompanies: selectedCompanies.length, companies: selectedCompanies, emails: selectedEmails, } }, [selectedRows])
const columns: DataTableColumnDef<Customer>[] = React.useMemo( () => [ { id: SYSTEM_COLUMN_IDS.SELECT, // 'id: "select"' triggers auto-detection for row selection size: 40, // Compact width for checkbox column 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", }, cell: ({ row }) => ( <div className="font-medium">{row.getValue("name")}</div> ), }, { accessorKey: "email", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> </DataTableColumnHeader> ), meta: { label: "Email", }, }, { accessorKey: "company", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> </DataTableColumnHeader> ), meta: { label: "Company", }, }, { accessorKey: "phone", header: () => ( <DataTableColumnHeader> <DataTableColumnTitle /> <DataTableColumnSortMenu variant={FILTER_VARIANTS.TEXT} /> </DataTableColumnHeader> ), meta: { label: "Phone", }, }, ], [], )
return ( <div className="w-full space-y-4"> <DataTableRoot data={data} columns={columns} state={{ globalFilter, sorting, columnFilters, columnVisibility, pagination, rowSelection, }} onGlobalFilterChange={setGlobalFilter} onSortingChange={setSorting} onColumnFiltersChange={setColumnFilters} onColumnVisibilityChange={setColumnVisibility} onPaginationChange={setPagination} onRowSelectionChange={setRowSelection} > <DataTableToolbarSection className="justify-between"> <DataTableSearchFilter placeholder="Search anything..." /> <DataTableViewMenu /> </DataTableToolbarSection> <SelectionBar selectedCount={selectedRows.length} onClear={clearSelection} /> <DataTable> <DataTableHeader /> <DataTableBody> <DataTableEmptyBody> <DataTableEmptyMessage> <DataTableEmptyIcon> <UserSearch className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No customers found</DataTableEmptyTitle> <DataTableEmptyDescription> There are no customers to display at this time. </DataTableEmptyDescription> </DataTableEmptyMessage> <DataTableEmptyFilteredMessage> <DataTableEmptyIcon> <SearchX className="size-12" /> </DataTableEmptyIcon> <DataTableEmptyTitle>No matches found</DataTableEmptyTitle> <DataTableEmptyDescription> Try adjusting your filters or search to find what you're looking for. </DataTableEmptyDescription> </DataTableEmptyFilteredMessage> </DataTableEmptyBody> </DataTableBody> </DataTable> <DataTablePagination pageSizeOptions={[5, 10, 20]} /> </DataTableRoot>
{/* State Display for demonstration */} <Card> <CardHeader> <CardTitle>Row Selection State</CardTitle> <CardDescription> Live view of the row selection table state with customer data </CardDescription> <CardAction> <Button variant="outline" size="sm" onClick={resetAllState}> Reset All State </Button> </CardAction> </CardHeader> <CardContent className="space-y-4"> <div className="grid gap-2 text-xs text-muted-foreground"> <div className="flex justify-between"> <span className="font-medium">Search Query:</span> <span className="text-foreground"> {typeof globalFilter === "string" ? globalFilter || "None" : "Mixed Filters"} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Total Customers:</span> <span className="text-foreground">{data.length}</span> </div>
<div className="flex justify-between"> <span className="font-medium">Selected Customers:</span> <span className="text-foreground"> {selectionMetrics.totalSelected} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Unique Companies:</span> <span className="text-foreground"> {selectionMetrics.uniqueCompanies} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Selection Percentage:</span> <span className="text-foreground"> {data.length > 0 ? Math.round( (selectionMetrics.totalSelected / data.length) * 100, ) : 0} % </span> </div>
<div className="flex justify-between"> <span className="font-medium">Sorting:</span> <span className="text-foreground"> {sorting.length > 0 ? sorting .map(s => `${s.id} ${s.desc ? "desc" : "asc"}`) .join(", ") : "None"} </span> </div>
<div className="flex justify-between"> <span className="font-medium">Page:</span> <span className="text-foreground"> {pagination.pageIndex + 1} (Size: {pagination.pageSize}) </span> </div>
<div className="flex justify-between"> <span className="font-medium">Hidden Columns:</span> <span className="text-foreground"> { Object.values(columnVisibility).filter(v => v === false) .length } </span> </div> </div>
{/* Selection Actions */} <div className="flex gap-2 border-t pt-4"> <Button variant="outline" size="sm" onClick={selectAllRows}> Select All </Button> <Button variant="outline" size="sm" onClick={selectNone}> Select None </Button> </div>
{/* Selected Companies List */} {selectionMetrics.companies.length > 0 && ( <div className="border-t pt-4"> <div className="mb-2 text-xs font-medium"> Selected Companies: </div> <div className="flex flex-wrap gap-1"> {selectionMetrics.companies.map(company => ( <Badge key={company} variant="outline" className="text-xs"> {company} </Badge> ))} </div> </div> )}
{/* Detailed state (collapsible) */} <details className="border-t pt-4"> <summary className="cursor-pointer text-xs font-medium hover:text-foreground"> View Full State Object </summary> <div className="mt-4 space-y-3 text-xs"> <div> <strong>Row Selection:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(rowSelection, null, 2)} </pre> </div> <div> <strong>Selected Customers:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify( selectedRows.map(customer => ({ id: customer.id, name: customer.name, email: customer.email, company: customer.company, })), null, 2, )} </pre> </div> <div> <strong>Sorting:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(sorting, null, 2)} </pre> </div> <div> <strong>Pagination:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(pagination, null, 2)} </pre> </div> <div> <strong>Column Visibility:</strong> <pre className="mt-1 overflow-auto rounded bg-muted p-2"> {JSON.stringify(columnVisibility, null, 2)} </pre> </div> </div> </details> </CardContent> </Card> </div> )}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”- Add the required components:
npx shadcn@latest add table input button dropdown-menu checkbox tooltip- Add
tanstack/react-tabledependency:
npm install @tanstack/react-table- Copy the DataTable components into your project. See the Installation Guide for detailed instructions.
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, 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" }, },]<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, 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> )}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"
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"
<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