Data Table

Build sortable, filterable tables with TanStack Table.

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
0 of 5 row(s) selected.

Rows per page

Page 1 of 1
tsx
"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-table

This 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.

PropTypeDefault
columnsColumnDef<TData, TValue>[]
tableTable<TData>
emptyMessagestring"No results."
classNamestring
<DataTable columns={columns} table={table} />

DataTableColumnHeader

Sortable, hideable column header. Renders plain text when the column cannot sort.

PropTypeDefault
columnColumn<TData, TValue>
titlestring
classNamestring
<DataTableColumnHeader column={column} title="Email" />

DataTablePagination

Provides page size, current page, first / previous / next / last navigation, and the selected row count.

PropTypeDefault
tableTable<TData>
classNamestring
<DataTablePagination table={table} />

DataTableViewOptions

Dropdown for toggling hideable columns. Its height and surface match Input.

PropTypeDefault
tableTable<TData>
classNamestring
<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.