DataTable
since v11.0.0DataTable — config-driven, branded, accessible data table on the TanStack
headless engine. Uncontrolled by default; opt-in controlled per feature.
Install
Add the package and import the component.
pnpm add @hey-mike/tungstenpnpm add @hey-mike/tungstenimport { DataTable } from '@hey-mike/tungsten';import { DataTable } from '@hey-mike/tungsten';Preview
Same fixtures used by the visual-regression suite.
Usage
apps/docs/app/snapshots/data-table/page.tsx
'use client';
import { DataTable, type DataTableColumn } from '@hey-mike/tungsten';
import { VariantGrid } from '../_components/VariantGrid';
import { HeroSpecimen } from '../_components/HeroSpecimen';
interface User {
id: string;
name: string;
email: string;
role: 'Admin' | 'Editor' | 'Viewer';
age: number;
}
const users: User[] = [
{ id: 'u1', name: 'Amara Reyes', email: 'amara@luoforge.com', role: 'Admin', age: 34 },
{ id: 'u2', name: 'Jonas Thorne', email: 'jonas@luoforge.com', role: 'Editor', age: 29 },
{ id: 'u3', name: 'Priya Kapoor', email: 'priya@luoforge.com', role: 'Viewer', age: 41 },
{ id: 'u4', name: 'Diego Alvarez', email: 'diego@luoforge.com', role: 'Editor', age: 27 },
{ id: 'u5', name: 'Mei Lin', email: 'mei@luoforge.com', role: 'Admin', age: 38 },
{ id: 'u6', name: 'Tomas Novak', email: 'tomas@luoforge.com', role: 'Viewer', age: 45 },
];
const roleTone: Record<User['role'], string> = {
Admin: 'bg-ink-1 text-on-ink',
Editor: 'border-stroke text-ink-2 border',
Viewer: 'border-stroke text-ink-3 border',
};
// Compact two-column slice for the gallery thumbnail (the full 4-column table
// overflows the 280px card).
const heroColumns: DataTableColumn<User>[] = [
{ id: 'name', header: 'Name', accessor: 'name' },
{ id: 'role', header: 'Role', accessor: 'role' },
];
const columns: DataTableColumn<User>[] = [
{ id: 'name', header: 'Name', accessor: 'name' },
{ id: 'email', header: 'Email', accessor: 'email' },
{
id: 'role',
header: 'Role',
accessor: 'role',
cell: (u) => (
<span
className={`inline-flex rounded-sm px-2 py-0.5 font-mono text-xs uppercase ${roleTone[u.role]}`}
>
{u.role}
</span>
),
},
{ id: 'age', header: 'Age', accessor: 'age', numeric: true, sortable: true },
];
export default function DataTableSnapshot() {
return (
<VariantGrid
title="DataTable"
hero={
<HeroSpecimen>
<DataTable
columns={heroColumns}
data={users.slice(0, 3)}
getRowId={(u) => u.id}
caption="Team members"
/>
</HeroSpecimen>
}
variants={[
{
label: 'users-admin',
node: (
<div className="w-[40rem]">
<DataTable
columns={columns}
data={users}
getRowId={(u) => u.id}
caption="Team members"
selectable
defaultSort={{ id: 'age', direction: 'asc' }}
defaultSelectedIds={['u1', 'u5']}
/>
</div>
),
},
]}
/>
);
}
'use client';
import { DataTable, type DataTableColumn } from '@hey-mike/tungsten';
import { VariantGrid } from '../_components/VariantGrid';
import { HeroSpecimen } from '../_components/HeroSpecimen';
interface User {
id: string;
name: string;
email: string;
role: 'Admin' | 'Editor' | 'Viewer';
age: number;
}
const users: User[] = [
{ id: 'u1', name: 'Amara Reyes', email: 'amara@luoforge.com', role: 'Admin', age: 34 },
{ id: 'u2', name: 'Jonas Thorne', email: 'jonas@luoforge.com', role: 'Editor', age: 29 },
{ id: 'u3', name: 'Priya Kapoor', email: 'priya@luoforge.com', role: 'Viewer', age: 41 },
{ id: 'u4', name: 'Diego Alvarez', email: 'diego@luoforge.com', role: 'Editor', age: 27 },
{ id: 'u5', name: 'Mei Lin', email: 'mei@luoforge.com', role: 'Admin', age: 38 },
{ id: 'u6', name: 'Tomas Novak', email: 'tomas@luoforge.com', role: 'Viewer', age: 45 },
];
const roleTone: Record<User['role'], string> = {
Admin: 'bg-ink-1 text-on-ink',
Editor: 'border-stroke text-ink-2 border',
Viewer: 'border-stroke text-ink-3 border',
};
// Compact two-column slice for the gallery thumbnail (the full 4-column table
// overflows the 280px card).
const heroColumns: DataTableColumn<User>[] = [
{ id: 'name', header: 'Name', accessor: 'name' },
{ id: 'role', header: 'Role', accessor: 'role' },
];
const columns: DataTableColumn<User>[] = [
{ id: 'name', header: 'Name', accessor: 'name' },
{ id: 'email', header: 'Email', accessor: 'email' },
{
id: 'role',
header: 'Role',
accessor: 'role',
cell: (u) => (
<span
className={`inline-flex rounded-sm px-2 py-0.5 font-mono text-xs uppercase ${roleTone[u.role]}`}
>
{u.role}
</span>
),
},
{ id: 'age', header: 'Age', accessor: 'age', numeric: true, sortable: true },
];
export default function DataTableSnapshot() {
return (
<VariantGrid
title="DataTable"
hero={
<HeroSpecimen>
<DataTable
columns={heroColumns}
data={users.slice(0, 3)}
getRowId={(u) => u.id}
caption="Team members"
/>
</HeroSpecimen>
}
variants={[
{
label: 'users-admin',
node: (
<div className="w-[40rem]">
<DataTable
columns={columns}
data={users}
getRowId={(u) => u.id}
caption="Team members"
selectable
defaultSort={{ id: 'age', direction: 'asc' }}
defaultSelectedIds={['u1', 'u5']}
/>
</div>
),
},
]}
/>
);
}
Props
Surface specific to <DataTable />.
| Prop | Type | Default | Description |
|---|---|---|---|
| columns* | DataTableColumn<TData>[] | — | — |
| data* | TData[] | — | — |
| getRowId* | (row: TData) => string | — | — |
| caption* | string | — | Required for an accessible table name (rendered as an sr-only <caption>). |
| getRowLabel | ((row: TData) => string) | — | Optional human-readable label for a row, used for the per-row selection
checkbox accessible name (Select ${getRowLabel(row)}). Falls back to the
opaque row id (Select row ${id}) when absent. |
| selectable | boolean | — | — |
| selectAllScope | "page" | "all" | page | — |
| pagination | DataTablePagination | — | — |
| defaultPagination | DataTablePagination | — | — |
| onPaginationChange | ((pagination: DataTablePagination) => void) | — | — |
| loading | boolean | — | — |
| error | ReactNode | — | — |
| emptyState | ReactNode | — | — |
| density | "comfortable" | "compact" | comfortable | — |
| stickyHeader | boolean | — | Freeze the header row at the top of a scrolling table. Requires a scroll container; one is added automatically. |
| stickyFirstColumn | boolean | — | Freeze the first data column at the left edge of a scrolling table. Requires a scroll container; one is added automatically. |
| virtualized | boolean | — | Window rows for large data sets (10k+). Disables the pagination UI — you scroll, not paginate. |
| estimatedRowHeight | number | — | Row height estimate for the virtualizer (px). Default 44. |
| enableColumnResizing | boolean | — | Allow dragging column borders to resize. Pointer-driven (keyboard resize is a follow-up). |
| enableColumnReordering | boolean | — | Allow dragging column headers to reorder columns. Pointer-driven (keyboard reorder is a follow-up). |
| manualPagination | boolean | — | — |
| rowCount | number | — | — |
| enableMultiSort | boolean | — | — |
| sort | DataTableSort | DataTableSort[] | null | — | — |
| defaultSort | DataTableSort | DataTableSort[] | null | — | — |
| onSortChange | ((sort: DataTableSort | null) => void) | ((sort: DataTableSort | null) => void) | ((sort: DataTableSort[]) => void) | ((sort: DataTableSort[]) => void) | — | — |
| selectedIds | string[] | — | — |
| defaultSelectedIds | string[] | — | — |
| onSelectedIdsChange | ((ids: string[]) => void) | ((ids: string[]) => void) | — | — |