Skip to content
LuoForge/Tungsten
Components/DataTable·Experimental

DataTable

since v11.0.0

DataTable — 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/tungsten
pnpm add @hey-mike/tungsten
import { 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 />.

PropTypeDefaultDescription
columns*DataTableColumn<TData>[]
data*TData[]
getRowId*(row: TData) => string
caption*stringRequired 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.
selectableboolean
selectAllScope"page" | "all"page
paginationDataTablePagination
defaultPaginationDataTablePagination
onPaginationChange((pagination: DataTablePagination) => void)
loadingboolean
errorReactNode
emptyStateReactNode
density"comfortable" | "compact"comfortable
stickyHeaderbooleanFreeze the header row at the top of a scrolling table. Requires a scroll container; one is added automatically.
stickyFirstColumnbooleanFreeze the first data column at the left edge of a scrolling table. Requires a scroll container; one is added automatically.
virtualizedbooleanWindow rows for large data sets (10k+). Disables the pagination UI — you scroll, not paginate.
estimatedRowHeightnumberRow height estimate for the virtualizer (px). Default 44.
enableColumnResizingbooleanAllow dragging column borders to resize. Pointer-driven (keyboard resize is a follow-up).
enableColumnReorderingbooleanAllow dragging column headers to reorder columns. Pointer-driven (keyboard reorder is a follow-up).
manualPaginationboolean
rowCountnumber
enableMultiSortboolean
sortDataTableSort | DataTableSort[] | null
defaultSortDataTableSort | DataTableSort[] | null
onSortChange((sort: DataTableSort | null) => void) | ((sort: DataTableSort | null) => void) | ((sort: DataTableSort[]) => void) | ((sort: DataTableSort[]) => void)
selectedIdsstring[]
defaultSelectedIdsstring[]
onSelectedIdsChange((ids: string[]) => void) | ((ids: string[]) => void)

Source