Selected rows counter example
Example of displaying selected row count:
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
Data Table prerequisites - Payment data type
The Payment type for data table examples has the following structure: id (string), amount (number), status (enum: 'pending' | 'processing' | 'success' | 'failed'), and email (string).
TanStack Table v9 features configuration
TanStack Table v9 is feature-based. Register features using tableFeatures() to declare which behavior to enable. Unregistered features are tree-shaken from the bundle. Register features: columnFilteringFeature, columnVisibilityFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature. Register row models: createFilteredRowModel(), createPaginatedRowModel(), createSortedRowModel(). Register filter functions under filterFns (e.g., includesString for email filtering) and sort functions under sortFns (e.g., text and alphanumeric). The core row model is always included automatically.
Column definition with accessor and display
Use createColumnHelper to define columns. Use columnHelper.accessor() for data columns that map to row properties. Use columnHelper.display() for columns without data binding. Example: columnHelper.accessor('email', { header: 'Email' }) defines a sortable/filterable email column.
Basic DataTable component
The DataTable component renders table rows using table.getRowModel().rows. It uses Table, TableHeader, TableHead, TableBody, TableCell, and TableRow components. TableHeader renders table.getFlatHeaders() with header.id and isRowHeader set to true for the first header. TableBody renders visible cells from each row with table.FlexRender for flexible cell content rendering.
Basic DataTable component example
'use client'
import { useTable, type ColumnDef, type RowData } from '@tanstack/react-table'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { features, type DataTableFeatures } from './data-table-features'
interface DataTableProps<TData extends RowData> {
columns: ColumnDef<DataTableFeatures, TData>[]
data: TData[]
}
export function DataTable<TData extends RowData>({
columns,
data,
}: DataTableProps<TData>) {
const table = useTable({
features,
data,
columns,
})
return (
<div className="overflow-hidden rounded-md border">
<Table>
<TableHeader>
{table.getFlatHeaders().map((header) => (
<TableHead
key={header.id}
id={header.id}
isRowHeader={header.index === 0}
>
{header.isPlaceholder ? null : (
<table.FlexRender header={header} />
)}
</TableHead>
))}
</TableHeader>
<TableBody renderEmptyState={() => 'No results.'}>
{table.getRowModel().rows.map((row) => (
<TableRow key={row.id} id={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
<table.FlexRender cell={cell} />
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}
Row actions dropdown example
Example of adding actions column in columns.tsx:
columnHelper.display({
id: 'actions',
cell: ({ row }) => {
const payment = row.original
return (
<DropdownMenuTrigger>
<Button variant="ghost" size="icon-xs">
<span className="sr-only">Open menu</span>
<MoreHorizontal />
</Button>
<DropdownMenu placement="bottom end" className="w-44">
<DropdownMenuGroup>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(payment.id)}
>
Copy payment ID
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenu>
</DropdownMenuTrigger>
)
},
})
Pagination in data tables
Pagination is automatically enabled when rowPaginationFeature and createPaginatedRowModel() are registered in the features object. By default, tables paginate into pages of 10 rows. Add pagination controls using table.previousPage() and table.nextPage() methods with disabled state based on table.getCanPreviousPage() and table.getCanNextPage().
Pagination controls example
Example of adding pagination controls to DataTable:
import { Button } from '@/components/ui/button'
export function DataTable<TData extends RowData>({
columns,
data,
}: DataTableProps<TData>) {
const table = useTable({
features,
data,
columns,
})
return (
<div>
<div className="overflow-hidden rounded-md border">
<Table>{ ... }</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
)
}
Sorting in data tables
Sorting is enabled when rowSortingFeature and createSortedRowModel() are registered in the features object. Wire up sorting by managing SortingState in component state and passing onSortingChange and state.sorting to useTable. Pass sortDescriptor and onSortChange props to the Table component. Make header cells sortable by rendering column headers with sorting controls.
Sorting implementation example
Example of implementing sorting in DataTable:
import * as React from 'react'
import { useTable, type SortingState } from '@tanstack/react-table'
export function DataTable<TData extends RowData>({
columns,
data,
}: DataTableProps<TData>) {
const [sorting, setSorting] = React.useState<SortingState>([])
const table = useTable({
features,
data,
columns,
onSortingChange: setSorting,
state: { sorting },
})
return (
<div>
<div className="overflow-hidden rounded-md border">
<Table
sortDescriptor={
sorting.length
? {
column: sorting[0].id,
direction: sorting[0].desc ? 'descending' : 'ascending',
}
: undefined
}
onSortChange={(sortDescriptor) => {
table.setSorting([
{
id: '' + sortDescriptor.column,
desc: sortDescriptor.direction === 'descending',
},
])
}}
>
{ ... }
</Table>
</div>
</div>
)
}
Sortable column header
Make a column header sortable by wrapping it with buttonVariants({ variant: 'ghost' }) and adding a sort icon. The table automatically handles ascending/descending sorting when the header is clicked.
Sortable header example
Example of making email column sortable in columns.tsx:
import { ArrowUpDown } from 'lucide-react'
import { buttonVariants } from '@/components/ui/button'
columnHelper.accessor('email', {
header: ({ column }) => {
return (
<div className={buttonVariants({ variant: 'ghost' })}>
Email
<ArrowUpDown className="ml-2 h-4 w-4" />
</div>
)
},
})
Filtering in data tables
Filtering is enabled when columnFilteringFeature and createFilteredRowModel() are registered in the features object. Wire up filtering by managing ColumnFiltersState in component state and passing onColumnFiltersChange and state.columnFilters to useTable. Add an Input component to set filter values using table.getColumn('columnId').setFilterValue().
Filtering implementation example
Example of implementing filtering in DataTable:
import * as React from 'react'
import { useTable, type ColumnFiltersState } from '@tanstack/react-table'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
export function DataTable<TData extends RowData>({
columns,
data,
}: DataTableProps<TData>) {
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
const table = useTable({
features,
data,
columns,
onColumnFiltersChange: setColumnFilters,
state: { columnFilters },
})
return (
<div>
<div className="flex items-center py-4">
<Input
placeholder="Filter emails..."
value={(table.getColumn('email')?.getFilterValue() as string) ?? ''}
onChange={(event) =>
table.getColumn('email')?.setFilterValue(event.target.value)
}
className="max-w-sm"
/>
</div>
<div className="overflow-hidden rounded-md border">
<Table>{ ... }</Table>
</div>
</div>
)
}
Column visibility in data tables
Column visibility is enabled when columnVisibilityFeature is registered in the features object. Wire up visibility by managing ColumnVisibilityState in component state and passing onColumnVisibilityChange and state.columnVisibility to useTable. Add a dropdown menu with DropdownMenuCheckboxItem to toggle visibility of individual columns.
Column visibility implementation example
Example of implementing column visibility in DataTable:
import * as React from 'react'
import { useTable, type ColumnVisibilityState } from '@tanstack/react-table'
import { Button } from '@/components/ui/button'
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
export function DataTable<TData extends RowData>({
columns,
data,
}: DataTableProps<TData>) {
const [columnVisibility, setColumnVisibility] = React.useState<ColumnVisibilityState>({})
const table = useTable({
features,
data,
columns,
onColumnVisibilityChange: setColumnVisibility,
state: { columnVisibility },
})
return (
<div>
<div className="flex items-center py-4">
<DropdownMenuTrigger>
<Button variant="outline" className="ml-auto">
Columns
</Button>
<DropdownMenu placement="bottom end">
<DropdownMenuGroup
selectionMode="multiple"
selectedKeys={
table
.getVisibleFlatColumns()
.filter((column) => column.getCanHide())
.map(column => column.id)
}
onSelectionChange={(keys) => {
table.setColumnVisibility(
Object.fromEntries(
table
.getAllFlatColumns()
.map((c) => [
c.id,
!c.getCanHide() || keys === 'all' || keys.has(c.id),
])
)
)
}}
>
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => (
<DropdownMenuItem
key={column.id}
id={column.id}
className="capitalize"
>
{column.id}
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</DropdownMenu>
</DropdownMenuTrigger>
</div>
<div className="overflow-hidden rounded-md border">
<Table>{ ... }</Table>
</div>
</div>
)
}
Row selection in data tables
Row selection is enabled when rowSelectionFeature is registered in the features object. Add a select column using columnHelper.display() with id 'select' containing Checkbox components with slot='selection'. Wire up selection by managing row selection state with onRowSelectionChange and state.rowSelection in useTable. Pass selectionMode='multiple' and selectedKeys to the Table component.
Row selection column example
Example of adding select column in columns.tsx:
import { Checkbox } from '@/components/ui/checkbox'
columnHelper.display({
id: 'select',
header: () => <Checkbox slot="selection" />,
cell: () => <Checkbox slot="selection" />,
enableSorting: false,
enableHiding: false,
})
Row selection implementation example
Example of implementing row selection in DataTable:
export function DataTable<TData extends RowData>({
columns,
data,
}: DataTableProps<TData>) {
const [rowSelection, setRowSelection] = React.useState({})
const table = useTable({
features,
data,
columns,
onRowSelectionChange: setRowSelection,
state: { rowSelection },
})
return (
<div>
<div className="overflow-hidden rounded-md border">
<Table
selectionMode="multiple"
selectedKeys={table.getSelectedRowModel().rows.map((row) => row.id)}
onSelectionChange={(selection) => {
if (selection === 'all') {
table.toggleAllRowsSelected()
} else {
table.setRowSelection(
Object.fromEntries([...selection].map((key) => [key, true]))
)
}
}}
/>
</div>
</div>
)
}
Cell formatting example
To format cells, update the column definition's cell property. For example, to format a currency amount: cell: ({ row }) => { const amount = parseFloat(row.getValue('amount')); const formatted = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount); return <div className='text-right font-medium'>{formatted}</div> }
Row actions using dropdown menu
Add row actions by creating a display column with columnHelper.display(). Access row data with row.original. Render a dropdown menu with DropdownMenuTrigger containing a Button, and DropdownMenuContent with DropdownMenuItems for actions.
Pagination setup in DataTable
Pagination is automatically enabled when rowPaginationFeature and createPaginatedRowModel() are included in the features object. Default page size is 10. Add pagination controls using table.previousPage() and table.nextPage() methods with table.getCanPreviousPage() and table.getCanNextPage() to check if pagination is available.
Sorting setup in DataTable
To enable sorting, import SortingState from @tanstack/react-table. Create state with React.useState<SortingState>([]). Pass onSortingChange and sorting state to useTable. In column definitions, create a sortable header by calling column.toggleSorting(column.getIsSorted() === 'asc') on click.
Filtering setup in DataTable
To enable filtering, import ColumnFiltersState from @tanstack/react-table. Create state with React.useState<ColumnFiltersState>([]). Pass onColumnFiltersChange and columnFilters state to useTable. Render an Input that gets/sets filter value using table.getColumn('columnName')?.getFilterValue() and table.getColumn('columnName')?.setFilterValue().
Column visibility toggle in DataTable
To enable column visibility toggling, import ColumnVisibilityState from @tanstack/react-table. Create state with React.useState<ColumnVisibilityState>({}). Pass onColumnVisibilityChange and columnVisibility state to useTable. Render a dropdown menu with DropdownMenuCheckboxItems for each column from table.getAllColumns().filter(column => column.getCanHide()). Use column.getIsVisible() to check visibility and column.toggleVisibility() to toggle.
Row selection setup in DataTable
To enable row selection, add a display column with checkboxes in both header and cell. In header: checkbox checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate')} onChange triggers table.toggleAllPageRowsSelected(). In cell: checkbox checked={row.getIsSelected()} onChange triggers row.toggleSelected(). Create state with useState({}) and pass onRowSelectionChange and rowSelection to useTable.
Accessing row data in column definitions
Access row data using row.original in the cell function of a column definition. This can be used to handle row actions like making API calls with the row's id.