Build data grids with TanStack Table and the arctis <Table /> primitives. Add selection, sorting, filtering, visibility, and pagination as needed.
| Status | Amount | Actions | ||
|---|---|---|---|---|
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 |
Rows per page
"use client"import * as React from "react"import { getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, type ColumnFiltersState, type SortingState, type VisibilityState,} from "@tanstack/react-table"import { DataTable, DataTablePagination, DataTableViewOptions,} from "@/components/ui/data-table"import { Input } from "@/components/ui/input"import { columns } from "./columns"import { payments } from "./payments"export function Example() { const [sorting, setSorting] = React.useState<SortingState>([]) const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( [], ) const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({}) const [rowSelection, setRowSelection] = React.useState({}) const table = useReactTable({ data: payments, columns, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), onColumnVisibilityChange: setColumnVisibility, onRowSelectionChange: setRowSelection, state: { sorting, columnFilters, columnVisibility, rowSelection, }, }) return ( <div className="w-full space-y-4"> <div className="flex w-full items-center justify-between gap-2"> <Input placeholder="Filter emails..." value={(table.getColumn("email")?.getFilterValue() as string) ?? ""} onChange={(event) => table.getColumn("email")?.setFilterValue(event.target.value) } className="w-auto max-w-sm" /> <DataTableViewOptions table={table} /> </div> <DataTable columns={columns} table={table} /> <DataTablePagination table={table} /> </div> )}Introduction
Tables rarely need the same columns, filters, or data source. Instead of working around a rigid component, compose the table you need with TanStack Table and <Table />.
Start with column definitions, then add selection, sorting, filtering, visibility, pagination, and row actions. The reusable pieces live in @arctis-sh/ui/data-table.
Installation
npx @arctis-sh/@arctis-sh/ui@latest add data-tableThis adds Table, Button, Dropdown Menu, Native Select, Checkbox, and Input, then installs the @tanstack/react-table peer.
Usage
"use client"import { getCoreRowModel, useReactTable,} from "@tanstack/react-table"import { DataTable } from "@/components/ui/data-table"const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(),})return <DataTable columns={columns} table={table} />Composition
div ├── Input ├── DataTableViewOptions ├── DataTable │ └── Table │ ├── TableHeader │ │ └── TableRow │ │ └── TableHead │ └── TableBody │ └── TableRow │ └── TableCell └── DataTablePagination
Prerequisites
The examples below use these payment rows:
type Payment = { id: string amount: number status: "pending" | "processing" | "success" | "failed" email: string}const payments: Payment[] = [ { id: "pay_8k2n1q", amount: 316, status: "success", email: "mina@example.com", }, // ...]Column definitions
Column definitions control structure, formatting, sorting, and filtering.
"use client"import type { ColumnDef } from "@tanstack/react-table"import type { Payment } from "./payments"export const columns: ColumnDef<Payment>[] = [ { accessorKey: "status", header: "Status", }, { accessorKey: "email", header: "Email", }, { accessorKey: "amount", header: () => <div className="text-right">Amount</div>, cell: ({ row }) => { const amount = Number.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 an actions column with a Dropdown Menu.
{ id: "actions", enableHiding: false, cell: ({ row }) => { const payment = row.original return ( <DropdownMenu> <DropdownMenuTrigger className={buttonVariants({ variant: "ghost", size: "icon-sm" })} aria-label="Open menu" > <span className="sr-only">Open menu</span> {/* icon */} </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuLabel>Actions</DropdownMenuLabel> <DropdownMenuItem onClick={() => navigator.clipboard.writeText(payment.id)} > Copy payment ID </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem>View customer</DropdownMenuItem> <DropdownMenuItem>View payment details</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ) },}Pagination
Enable TanStack pagination, then render DataTablePagination.
const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(),})<DataTable columns={columns} table={table} /><DataTablePagination table={table} />Sorting
Track sorting state and use DataTableColumnHeader for a sortable header.
const [sorting, setSorting] = React.useState<SortingState>([])const table = useReactTable({ data, columns, onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), state: { sorting },}){ accessorKey: "email", header: ({ column }) => ( <DataTableColumnHeader column={column} title="Email" /> ),}Filtering
Filter a column with a controlled Input.
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])const table = useReactTable({ data, columns, onColumnFiltersChange: setColumnFilters, getFilteredRowModel: getFilteredRowModel(), getCoreRowModel: getCoreRowModel(), state: { columnFilters },})<Input placeholder="Filter emails..." value={(table.getColumn("email")?.getFilterValue() as string) ?? ""} onChange={(event) => table.getColumn("email")?.setFilterValue(event.target.value) } className="w-auto max-w-sm"/>Visibility
Toggle columns with DataTableViewOptions. Hidden columns keep their width so the layout does not jump.
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})const table = useReactTable({ data, columns, onColumnVisibilityChange: setColumnVisibility, getCoreRowModel: getCoreRowModel(), state: { columnVisibility },})<div className="flex w-full items-center justify-between gap-2"> <Input className="w-auto max-w-sm" placeholder="Filter emails..." /> <DataTableViewOptions table={table} /></div>Row Selection
Add a select column with Checkbox, then track rowSelection.
{ 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,}const [rowSelection, setRowSelection] = React.useState({})const table = useReactTable({ data, columns, onRowSelectionChange: setRowSelection, getCoreRowModel: getCoreRowModel(), state: { rowSelection },})DataTablePagination shows the number of selected rows.
API Reference
DataTable
Renders the bordered table from a TanStack table instance. Hidden columns stay in the layout at zero opacity so siblings do not shift.
| Prop | Type | Default |
|---|---|---|
| columns | ColumnDef<TData, TValue>[] | — |
| table | Table<TData> | — |
| emptyMessage | string | "No results." |
| className | string | — |
<DataTable columns={columns} table={table} />DataTableColumnHeader
Sortable, hideable column header. Renders plain text when the column cannot sort.
| Prop | Type | Default |
|---|---|---|
| column | Column<TData, TValue> | — |
| title | string | — |
| className | string | — |
<DataTableColumnHeader column={column} title="Email" />DataTablePagination
Provides page size, current page, first / previous / next / last navigation, and the selected row count.
| Prop | Type | Default |
|---|---|---|
| table | Table<TData> | — |
| className | string | — |
<DataTablePagination table={table} />DataTableViewOptions
Dropdown for toggling hideable columns. Its height and surface match Input.
| Prop | Type | Default |
|---|---|---|
| table | Table<TData> | — |
| className | string | — |
<DataTableViewOptions table={table} />TanStack Table
DataTable is a thin layout layer. Sorting, filtering, pagination, and selection come from TanStack Table. Its documentation covers the full headless API, including useReactTable, row models, and column helpers.