Data Table
Powerful table and datagrids built using TanStack Table.
| Status | Amount | |||
|---|---|---|---|---|
success | ken99@example.com | $316.00 | ||
success | Abe45@example.com | $242.00 | ||
processing | Monserrat44@example.com | $837.00 | ||
success | Silas22@example.com | $874.00 | ||
failed | carmella@example.com | $721.00 |
Introduction
Every data table or datagrid is unique. They behave differently, have specific sorting and filtering requirements, and work with different data sources.
It does not make sense to combine all of these variations into a single component. Instead of a data-table component, this page is a guide for building your own with the Table primitive and TanStack Table.
Table of Contents
- Set up Table Features
- Basic Table
- Row Actions
- Pagination
- Sorting
- Filtering
- Visibility
- Row Selection
Installation
1. Add the Table component to your project:
pnpm dlx cubix@latest add table2. Add the @tanstack/react-table dependency. This guide uses TanStack Table v9:
pnpm add @tanstack/react-tablePrerequisites
We are going to build a table to show recent payments. Here is what our data looks like:
type Payment = {
id: string
amount: number
status: "pending" | "processing" | "success" | "failed"
email: string
}
export const payments: Payment[] = [
{
id: "728ed52f",
amount: 100,
status: "pending",
email: "m@example.com",
},
{
id: "489e1d42",
amount: 125,
status: "processing",
email: "example@gmail.com",
},
]Project Structure
app
└── payments
├── columns.tsx
├── data-table-features.ts
├── data-table.tsx
└── page.tsxcolumns.tsx- column definitionsdata-table-features.ts- shared features objectdata-table.tsx- table componentpage.tsx- fetch data and render
Set up Table Features
TanStack Table v9 is feature-based: you opt into sorting, filtering, pagination, and more with tableFeatures(). Anything you do not list is tree-shaken out of your bundle.
import {
columnFilteringFeature,
columnVisibilityFeature,
createFilteredRowModel,
createPaginatedRowModel,
createSortedRowModel,
filterFn_includesString,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
sortFn_alphanumeric,
sortFn_text,
tableFeatures,
} from "@tanstack/react-table"
export const features = tableFeatures({
columnFilteringFeature,
columnVisibilityFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
filteredRowModel: createFilteredRowModel(),
paginatedRowModel: createPaginatedRowModel(),
sortedRowModel: createSortedRowModel(),
filterFns: { includesString: filterFn_includesString },
sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text },
})
export type DataTableFeatures = typeof featuresBasic Table
Column Definitions
"use client"
import { createColumnHelper } from "@tanstack/react-table"
import { type DataTableFeatures } from "./data-table-features"
export type Payment = {
id: string
amount: number
status: "pending" | "processing" | "success" | "failed"
email: string
}
const columnHelper = createColumnHelper<DataTableFeatures, Payment>()
export const columns = columnHelper.columns([
columnHelper.accessor("status", {
header: "Status",
}),
columnHelper.accessor("email", {
header: "Email",
}),
columnHelper.accessor("amount", {
header: "Amount",
}),
])DataTable component
"use client"
import { useTable, type ColumnDef, type RowData } from "@tanstack/react-table"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/cubix/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.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder ? null : (
<table.FlexRender header={header} />
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
<table.FlexRender cell={cell} />
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)
}Render the table
import { columns, type Payment } from "./columns"
import { DataTable } from "./data-table"
async function getData(): Promise<Payment[]> {
return [
{
id: "728ed52f",
amount: 100,
status: "pending",
email: "m@example.com",
},
]
}
export default async function DemoPage() {
const data = await getData()
return (
<div className="container mx-auto py-10">
<DataTable columns={columns} data={data} />
</div>
)
}Cell Formatting
Format the amount cell as currency and align it to the right.
columnHelper.accessor("amount", {
header: () => <div className="text-right">Amount</div>,
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
Add row actions with a DropdownMenu. Access row data with row.original.
columnHelper.display({
id: "actions",
cell: ({ row }) => {
const payment = row.original
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="ghost" size="icon-xs" />}>
<span className="sr-only">Open menu</span>
<MoreHorizontalIcon />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(payment.id)}
>
Copy payment ID
</DropdownMenuItem>
<DropdownMenuItem>View customer</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
},
})Pagination
Because the features object includes rowPaginationFeature and createPaginatedRowModel(), the table paginates into pages of 10 by default. Add controls with previousPage() and nextPage():
<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>Sorting
Wire sorting state, then make a header cell sortable:
const [sorting, setSorting] = React.useState<SortingState>([])
const table = useTable({
features,
data,
columns,
onSortingChange: setSorting,
state: { sorting },
})columnHelper.accessor("email", {
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Email
<ArrowUpDownIcon />
</Button>
)
},
})Filtering
Add a search input to filter emails:
<Input
placeholder="Filter emails..."
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("email")?.setFilterValue(event.target.value)
}
className="max-w-sm"
/>Visibility
Let users toggle columns with a dropdown:
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="outline" />}>
Columns <ChevronDownIcon />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => (
<DropdownMenuCheckboxItem
key={column.id}
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>Row Selection
Add a select column with checkboxes:
columnHelper.display({
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
indeterminate={
table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected()
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
})API Reference
See the TanStack Table documentation for more information.