Data Table
Sortable data table powered by TanStack Table and the Foundations Table primitive
| May 9 | M | -$2,200.00 | Ops / Payroll | Working Capital Loan |
| May 9 | O | -$662.70 | Credit account | Jane B. ··0330 |
| May 9 | O | $563.94 | Credit account | Jane B. ··0330 |
| May 9 | M | $2,760.75 | Ops / Payroll | Jane B. ··3745 |
| May 9 | L | $25.89 | Credit account | Jane B. ··5555 |
import type { ColumnDef } from '@tanstack/react-table';
import { Avatar } from '@/components/avatar';
import { DataTable } from '@/components/data-table';
import { Surface } from '@/components/surface';
type Txn = {
id: string;
date: string;
payee: string;
amount: number;
account: string;
method: string;
};
const data: Txn[] = [
{
id: '1',
date: 'May 9',
payee: 'Mercury Working Capital',
amount: -2200,
account: 'Ops / Payroll',
method: 'Working Capital Loan',
},
{
id: '2',
date: 'May 9',
payee: 'Office Stop Co.',
amount: -662.7,
account: 'Credit account',
method: 'Jane B. ··0330',
},
{
id: '3',
date: 'May 9',
payee: 'Office Stop Co.',
amount: 563.94,
account: 'Credit account',
method: 'Jane B. ··0330',
},
{
id: '4',
date: 'May 9',
payee: 'Milgram Brokerage',
amount: 2760.75,
account: 'Ops / Payroll',
method: 'Jane B. ··3745',
},
{
id: '5',
date: 'May 9',
payee: "Lily's Eatery",
amount: 25.89,
account: 'Credit account',
method: 'Jane B. ··5555',
},
];
const fmt = (n: number) =>
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(n);
const columns: ColumnDef<Txn>[] = [
{ accessorKey: 'date', header: 'Date' },
{
accessorKey: 'payee',
header: 'To/From',
cell: ({ row }) => (
<div className="flex items-center gap-2">
<Avatar size="sm">
<Avatar.Fallback>{row.original.payee.charAt(0)}</Avatar.Fallback>
</Avatar>
<span>{row.original.payee}</span>
</div>
),
},
{
accessorKey: 'amount',
header: 'Amount',
meta: { align: 'end' },
cell: ({ getValue }) => {
const v = getValue<number>();
return (
<span
className={
v < 0 ? 'text-foreground' : 'font-medium text-success tabular-nums'
}
>
{fmt(v)}
</span>
);
},
},
{ accessorKey: 'account', header: 'Account' },
{ accessorKey: 'method', header: 'Method' },
];
export default function DataTablePreview() {
return (
<Surface elevation="default" padding="none" className="w-full">
<DataTable columns={columns} data={data} onRowClick={() => {}} />
</Surface>
);
} Dependencies
Source Code
'use client';
import {
type ColumnDef,
flexRender,
getCoreRowModel,
getSortedRowModel,
type SortingState,
useReactTable,
} from '@tanstack/react-table';
import { useState } from 'react';
import { Table } from '@/components/table';
import { cn } from '@/lib/utils/classnames';
interface DataTableProps<TData, TValue>
extends Omit<React.ComponentPropsWithRef<'table'>, 'children'> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
density?: 'sm' | 'md';
/** Sticky header — only useful when the table is inside a scroll container. */
stickyHeader?: boolean;
/** Called when a body row is clicked. Receives the original row data. */
onRowClick?: (row: TData) => void;
/** Empty-state placeholder when `data.length === 0`. */
emptyState?: React.ReactNode;
}
const DataTable = <TData, TValue>({
ref,
columns,
data,
density = 'md',
stickyHeader,
onRowClick,
emptyState,
className,
...rest
}: DataTableProps<TData, TValue>) => {
const [sorting, setSorting] = useState<SortingState>([]);
const table = useReactTable({
data,
columns,
state: { sorting },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});
return (
<Table ref={ref} density={density} className={className} {...rest}>
<Table.Header sticky={stickyHeader}>
{table.getHeaderGroups().map((headerGroup) => (
<Table.Row key={headerGroup.id}>
{headerGroup.headers.map((header) => {
const canSort = header.column.getCanSort();
const sortDir = header.column.getIsSorted();
const align = (
header.column.columnDef.meta as
| { align?: 'start' | 'center' | 'end' }
| undefined
)?.align;
if (canSort) {
const toggle = header.column.getToggleSortingHandler();
return (
<Table.SortableHead
key={header.id}
align={align}
sort={sortDir === false ? false : sortDir}
onSort={(event) => toggle?.(event)}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</Table.SortableHead>
);
}
return (
<Table.Head key={header.id} align={align}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</Table.Head>
);
})}
</Table.Row>
))}
</Table.Header>
<Table.Body>
{table.getRowModel().rows.length === 0 ? (
<Table.Row>
<Table.Cell
colSpan={columns.length}
className="py-12 text-center text-foreground-secondary"
>
{emptyState ?? 'No results.'}
</Table.Cell>
</Table.Row>
) : (
table.getRowModel().rows.map((row) => (
<Table.Row
key={row.id}
onClick={onRowClick ? () => onRowClick(row.original) : undefined}
className={cn(onRowClick && 'cursor-pointer')}
>
{row.getVisibleCells().map((cell) => {
const align = (
cell.column.columnDef.meta as
| { align?: 'start' | 'center' | 'end' }
| undefined
)?.align;
return (
<Table.Cell key={cell.id} align={align}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Table.Cell>
);
})}
</Table.Row>
))
)}
</Table.Body>
</Table>
);
};
export type { DataTableProps };
export { DataTable }; DataTable is a thin integration of TanStack Table
on top of the Foundations Table primitive. Pass columns and data and you
get sorting, alignment, sticky headers, click-through rows, and a built-in
empty state — without giving up Foundations’ table styling.
Anatomy
const columns: ColumnDef<Row>[] = [
{ accessorKey: 'name', header: 'Name' },
{
accessorKey: 'amount',
header: 'Amount',
meta: { align: 'end' },
cell: ({ getValue }) => formatMoney(getValue<number>()),
},
];
<DataTable columns={columns} data={rows} onRowClick={...} />
API Reference
| Prop | Default | Type |
|---|---|---|
columns | - | ColumnDef[] |
data | - | TData[] |
density | "md" | "sm""md" |
stickyHeader | - | boolean |
onRowClick | - | (row) => void |
emptyState | - | ReactNode |
Per-column alignment is set via columnDef.meta.align:
'start' | 'center' | 'end'.
Examples
Default
| May 9 | M | -$2,200.00 | Ops / Payroll | Working Capital Loan |
| May 9 | O | -$662.70 | Credit account | Jane B. ··0330 |
| May 9 | O | $563.94 | Credit account | Jane B. ··0330 |
| May 9 | M | $2,760.75 | Ops / Payroll | Jane B. ··3745 |
| May 9 | L | $25.89 | Credit account | Jane B. ··5555 |
import type { ColumnDef } from '@tanstack/react-table';
import { Avatar } from '@/components/avatar';
import { DataTable } from '@/components/data-table';
import { Surface } from '@/components/surface';
type Txn = {
id: string;
date: string;
payee: string;
amount: number;
account: string;
method: string;
};
const data: Txn[] = [
{
id: '1',
date: 'May 9',
payee: 'Mercury Working Capital',
amount: -2200,
account: 'Ops / Payroll',
method: 'Working Capital Loan',
},
{
id: '2',
date: 'May 9',
payee: 'Office Stop Co.',
amount: -662.7,
account: 'Credit account',
method: 'Jane B. ··0330',
},
{
id: '3',
date: 'May 9',
payee: 'Office Stop Co.',
amount: 563.94,
account: 'Credit account',
method: 'Jane B. ··0330',
},
{
id: '4',
date: 'May 9',
payee: 'Milgram Brokerage',
amount: 2760.75,
account: 'Ops / Payroll',
method: 'Jane B. ··3745',
},
{
id: '5',
date: 'May 9',
payee: "Lily's Eatery",
amount: 25.89,
account: 'Credit account',
method: 'Jane B. ··5555',
},
];
const fmt = (n: number) =>
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(n);
const columns: ColumnDef<Txn>[] = [
{ accessorKey: 'date', header: 'Date' },
{
accessorKey: 'payee',
header: 'To/From',
cell: ({ row }) => (
<div className="flex items-center gap-2">
<Avatar size="sm">
<Avatar.Fallback>{row.original.payee.charAt(0)}</Avatar.Fallback>
</Avatar>
<span>{row.original.payee}</span>
</div>
),
},
{
accessorKey: 'amount',
header: 'Amount',
meta: { align: 'end' },
cell: ({ getValue }) => {
const v = getValue<number>();
return (
<span
className={
v < 0 ? 'text-foreground' : 'font-medium text-success tabular-nums'
}
>
{fmt(v)}
</span>
);
},
},
{ accessorKey: 'account', header: 'Account' },
{ accessorKey: 'method', header: 'Method' },
];
export default function DataTablePreview() {
return (
<Surface elevation="default" padding="none" className="w-full">
<DataTable columns={columns} data={data} onRowClick={() => {}} />
</Surface>
);
} Previous
Container
Next
Date Picker