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>
; useaccessorKey
for base fields. - For sortable columns, render
header: ({ column }) => <DataTableColumnHeader column={column} title="…" />
. - Keep heavy logic out of
cell
renderers - precompute or memoize if needed.
Sorting and filtering
- Sorting is opt-in per column;
DataTableColumnHeader
respectsgetCanSort()
. - The
Searchbar
updatestable.getColumn('…').setFilterValue(value)
for a “contains” text filter on its target column. - Add more filters in
DataTable.Actions
and 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.