Skip to content

Row Selection Table

Add row selection with checkboxes for bulk actions.

Open in
"use client"
import * as React 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 { 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 { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"
import {
DataTableEmptyIcon,
DataTableEmptyMessage,
DataTableEmptyFilteredMessage,
DataTableEmptyTitle,
DataTableEmptyDescription,
} from "@/components/niko-table/components/data-table-empty-state"
import { DataTableSelectionBar } from "@/components/niko-table/components/data-table-selection-bar"
import { DataTableViewMenu } from "@/components/niko-table/components/data-table-view-menu"
import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"
import { DataTablePagination } from "@/components/niko-table/components/data-table-pagination"
import {
SYSTEM_COLUMN_IDS,
FILTER_VARIANTS,
} from "@/components/niko-table/lib/constants"
import { useDataTable } from "@/components/niko-table/core/data-table-context"
import { exportTableToCSV } from "@/components/niko-table/filters/table-export-button"
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 context
function 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&apos;re looking for.
</DataTableEmptyDescription>
</DataTableEmptyFilteredMessage>
</DataTableEmptyBody>
</DataTableBody>
</DataTable>
<DataTablePagination pageSizeOptions={[5, 10, 20]} />
</DataTableRoot>
)
}
Preview with Controlled State
Open in
"use client"
import * as React from "react"
import { useState } from "react"
import type {
PaginationState,
SortingState,
ColumnFiltersState,
VisibilityState,
RowSelectionState,
} from "@tanstack/react-table"
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 { 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 { DataTableToolbarSection } from "@/components/niko-table/components/data-table-toolbar-section"
import {
DataTableEmptyIcon,
DataTableEmptyMessage,
DataTableEmptyFilteredMessage,
DataTableEmptyTitle,
DataTableEmptyDescription,
} from "@/components/niko-table/components/data-table-empty-state"
import { DataTableSelectionBar } from "@/components/niko-table/components/data-table-selection-bar"
import { DataTableViewMenu } from "@/components/niko-table/components/data-table-view-menu"
import { DataTableSearchFilter } from "@/components/niko-table/components/data-table-search-filter"
import { DataTablePagination } from "@/components/niko-table/components/data-table-pagination"
import {
SYSTEM_COLUMN_IDS,
FILTER_VARIANTS,
} from "@/components/niko-table/lib/constants"
import { useDataTable } from "@/components/niko-table/core/data-table-context"
import { exportTableToCSV } from "@/components/niko-table/filters/table-export-button"
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 context
function 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&apos;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>
)
}

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",
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)