Overview
The Data Table wraps TanStack Table v8 (headless) with shadcn-styled markup and a small toolbar API. You get consistent visuals, sorting, a simple text filter, and an actions slot. We use TanStack Table for table logic and shadcn/ui for primitives.
Helpful references:
- shadcn/ui Data Table - patterns and styling
- TanStack Table (Headless UI) - why the core is headless and composable
Imports
From @/components/DataTable:
DataTable- main component (state + rendering)DataTable.Header- toolbar row above the tableDataTable.Searchbar- text filter bound to one columnDataTable.Actions- right-aligned slot for buttonsDataTableColumnHeader- sortable header rendererDataTableBadge- lightweight pill for status cells
From @/components/ui/Table: Table, TableHeader, TableHead, TableBody, TableRow, TableCell
Core API
<DataTable data columns>- acceptsdata: T[]andcolumns: ColumnDef<T>[]. Internally wires sorting, filtering, and the core row model.<DataTable.Header>- optional toolbar; place filters and action buttons here.<DataTable.Searchbar searchColumn="name">- one-field “contains” filter for a specific column.<DataTable.Actions>- right-aligned slot for things like “Create” or dialogs.DataTableColumnHeader- drop-in header for sortable columns (shows/toggles sort arrows).DataTableBadge- small tokenized label for statuses.
Minimal example
'use client';
import * as React from 'react';
import type { ColumnDef } from '@tanstack/react-table';
import Link from 'next/link';
import { DataTable } from '@/components/DataTable';
import { DataTableBadge } from '@/components/DataTable/DataTableBadge';
import { DataTableColumnHeader } from '@/components/DataTable/DataTableColumnHeader';
import { Button } from '@/components/ui/Button';
type Row = { id: string; name: string; status: string; createdAt: string };
const columns: ColumnDef<Row>[] = [
{
accessorKey: 'name',
header: 'Project',
cell: ({ row }) => <Link href={`/projects/${row.original.id}`} className="hover:underline">{row.original.name}</Link>,
},
{
accessorKey: 'status',
header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
cell: ({ row }) => <DataTableBadge>{row.original.status}</DataTableBadge>,
},
{
accessorKey: 'createdAt',
header: 'Created',
cell: ({ row }) => new Date(row.original.createdAt).toLocaleDateString(),
},
];
export function ProjectsTable({ data }: { data: Row[] }) {
return (
<DataTable data={data} columns={columns}>
<DataTable.Header>
<DataTable.Searchbar searchColumn="name" />
<DataTable.Actions>
<Button variant="outline">Create</Button>
</DataTable.Actions>
</DataTable.Header>
</DataTable>
);
}Tip: Esc clears the search input when a filter is active.
Columns
- Define columns with TanStack’s
ColumnDef<T>; useaccessorKeyfor base fields. - For sortable columns, render
header: ({ column }) => <DataTableColumnHeader column={column} title="…" />. - Keep heavy logic out of
cellrenderers - precompute or memoize if needed.
Sorting and filtering
- Sorting is opt-in per column;
DataTableColumnHeaderrespectsgetCanSort(). - The
Searchbarupdatestable.getColumn('…').setFilterValue(value)for a “contains” text filter on its target column. - Add more filters in
DataTable.Actionsand wire them to TanStack’s column filter state.
Empty, paging, selection
- If there are no rows after filtering, the table shows No results.
- Pagination is not bundled - paginate server-side and pass the current slice.
- Row selection isn’t wired by default; add it via TanStack selection state when needed.
Accessibility
Uses semantic table markup from shadcn/ui. Sort controls are real buttons with visible labels. Icon-only actions should have accessible text.
Notes and tips
- Keep column keys stable to help TanStack memoization.
- Prefer tokens and Tailwind utilities over hardcoded colors.
- The table container is horizontally scrollable on narrow screens.