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:

Imports

From @/components/DataTable:

  • DataTable - main component (state + rendering)
  • DataTable.Header - toolbar row above the table
  • DataTable.Searchbar - text filter bound to one column
  • DataTable.Actions - right-aligned slot for buttons
  • DataTableColumnHeader - sortable header renderer
  • DataTableBadge - lightweight pill for status cells

From @/components/ui/Table: Table, TableHeader, TableHead, TableBody, TableRow, TableCell

Core API

  • <DataTable data columns> - accepts data: T[] and columns: 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>; use accessorKey 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 respects getCanSort().
  • The Searchbar updates table.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.