PixelDataTable

TanStack-powered surface-aware data table with controlled sorting, filtering, pagination, row selection, column visibility, density, sticky header, loading skeletons and empty state.

  • Fully controlled state for sorting, filtering, pagination, row selection and column visibility
  • Built on TanStack Table with re-exported ColumnDef and createColumnHelper for typed columns
  • Auto-injected selection column with indeterminate header checkbox when row selection is enabled
  • Three density presets, sticky header, skeleton loading rows and configurable empty state
  • Surface-aware (pixel or linear) with focus-ring tokens and retro design system colors
Status
stable
Category
data
Since
v1.9.0

API

import { PixelDataTable } from '@pxlkit/ui-kit';
PropTypeDefaultDescription
data (required)TData[]—Row data.
columns (required)ColumnDef<TData, TValue>[]—TanStack column definitions.
sorting{ id: string; desc: boolean }[]—Sort state; leave unset to let the table sort.
onSortingChange(next: { id: string; desc: boolean }[]) => void—Called with the new sort, after a click on a sortable header.
filteringRecord<string, string>—Column filters, column id → value.
onFilteringChange(next: Record<string, string>) => void—Called with the new column filters.
pagination{ pageIndex: number; pageSize: number }—Page; the pagination bar shows once it is set.
onPaginationChange(next: { pageIndex: number; pageSize: number }) => void—Called with the new page, after a page button, a page-size change or a reset to the first page.
rowSelectionRecord<string, boolean>—Selected row ids → true; the selection column shows once it is set.
onRowSelectionChange(next: Record<string, boolean>) => void—Called with the new row selection, after a checkbox changes.
columnVisibilityRecord<string, boolean>—Column id → shown.
onColumnVisibilityChange(next: Record<string, boolean>) => void—Called with the new column visibility.
getRowId(row: TData, idx: number) => string—A stable id per row; the row's index by default.
density'compact' | 'normal' | 'comfortable''normal'Cell padding scale.
stickyHeaderbooleanfalseSticks the header to the top of the scroll container.
loadingbooleanfalseShows skeleton rows instead of the data.
emptyStateReact.ReactNode—Shown in a full-width cell when there are no rows; "No data." by default.
onRowClick(row: TData) => void—Called with a row's data when it is clicked, or activated with Enter or Space; with it, rows show a pointer and take focus.
surface'pixel' | 'linear'—Surface override; defaults to the nearest provider.
classNamestring—Extra classes on the wrapper.
borderedbooleantrueRender with surface-aware border + radius chrome. Defaults to true — data table needs visible chrome.

ref points to <div>.

Accessibility

WCAG target: 2.1 AA

  • table

Renders semantic <table>, <thead>, <tbody>, <th scope="col"> and aria-sort on sortable column headers. Row selection checkboxes carry aria-label "Select row {id}" and a header checkbox labelled "Select all rows" with indeterminate state. Loading body exposes aria-busy and a role="status" with aria-live="polite" announcement. Pagination buttons declare aria-label and the page counter is announced with aria-live.

Keyboard

KeyActionWhen
EnterActivates a sortable column header button to cycle sort direction, or the focused clickable row (onRowClick)—
SpaceToggles the selection checkbox when focused in the selection column, or activates the focused clickable row—
TabMoves focus between sort buttons, selection checkboxes, clickable rows and pagination controls—

Usage

import { PixelDataTable, createColumnHelper, type ColumnDef } from '@pxlkit/ui-kit';

type Row = {
  id: string;
  name: string;
  role: string;
  status: string;
};

const rows: Row[] = [
  { id: '1', name: 'Alice', role: 'Engineer', status: 'active' },
  { id: '2', name: 'Bob', role: 'Designer', status: 'idle' },
  { id: '3', name: 'Carol', role: 'PM', status: 'active' },
  { id: '4', name: 'Dan', role: 'Engineer', status: 'active' },
  { id: '5', name: 'Eve', role: 'Designer', status: 'idle' },
];

const ch = createColumnHelper<Row>();

const columns: ColumnDef<Row, unknown>[] = [
  ch.accessor('name', { header: 'Name' }) as ColumnDef<Row, unknown>,
  ch.accessor('role', { header: 'Role' }) as ColumnDef<Row, unknown>,
  ch.accessor('status', { header: 'Status' }) as ColumnDef<Row, unknown>,
];

export function Default() {
  return <PixelDataTable<Row> data={rows} columns={columns} />;
}

Examples

Default

import { PixelDataTable, createColumnHelper, type ColumnDef } from '@pxlkit/ui-kit';

type Row = {
  id: string;
  name: string;
  role: string;
  status: string;
};

const rows: Row[] = [
  { id: '1', name: 'Alice', role: 'Engineer', status: 'active' },
  { id: '2', name: 'Bob', role: 'Designer', status: 'idle' },
  { id: '3', name: 'Carol', role: 'PM', status: 'active' },
  { id: '4', name: 'Dan', role: 'Engineer', status: 'active' },
  { id: '5', name: 'Eve', role: 'Designer', status: 'idle' },
];

const ch = createColumnHelper<Row>();

const columns: ColumnDef<Row, unknown>[] = [
  ch.accessor('name', { header: 'Name' }) as ColumnDef<Row, unknown>,
  ch.accessor('role', { header: 'Role' }) as ColumnDef<Row, unknown>,
  ch.accessor('status', { header: 'Status' }) as ColumnDef<Row, unknown>,
];

export function Default() {
  return <PixelDataTable<Row> data={rows} columns={columns} />;
}

Pixel surface

import { PixelDataTable, createColumnHelper, type ColumnDef } from '@pxlkit/ui-kit';

type Row = {
  id: string;
  name: string;
  role: string;
  status: string;
};

const rows: Row[] = [
  { id: '1', name: 'Alice', role: 'Engineer', status: 'active' },
  { id: '2', name: 'Bob', role: 'Designer', status: 'idle' },
  { id: '3', name: 'Carol', role: 'PM', status: 'active' },
  { id: '4', name: 'Dan', role: 'Engineer', status: 'active' },
  { id: '5', name: 'Eve', role: 'Designer', status: 'idle' },
];

const ch = createColumnHelper<Row>();

const columns: ColumnDef<Row, unknown>[] = [
  ch.accessor('name', { header: 'Name' }) as ColumnDef<Row, unknown>,
  ch.accessor('role', { header: 'Role' }) as ColumnDef<Row, unknown>,
  ch.accessor('status', { header: 'Status' }) as ColumnDef<Row, unknown>,
];

export function PixelSurface() {
  return <PixelDataTable<Row> data={rows} columns={columns} surface="pixel" />;
}

Linear surface

import { PixelDataTable, createColumnHelper, type ColumnDef } from '@pxlkit/ui-kit';

type Row = {
  id: string;
  name: string;
  role: string;
  status: string;
};

const rows: Row[] = [
  { id: '1', name: 'Alice', role: 'Engineer', status: 'active' },
  { id: '2', name: 'Bob', role: 'Designer', status: 'idle' },
  { id: '3', name: 'Carol', role: 'PM', status: 'active' },
  { id: '4', name: 'Dan', role: 'Engineer', status: 'active' },
  { id: '5', name: 'Eve', role: 'Designer', status: 'idle' },
];

const ch = createColumnHelper<Row>();

const columns: ColumnDef<Row, unknown>[] = [
  ch.accessor('name', { header: 'Name' }) as ColumnDef<Row, unknown>,
  ch.accessor('role', { header: 'Role' }) as ColumnDef<Row, unknown>,
  ch.accessor('status', { header: 'Status' }) as ColumnDef<Row, unknown>,
];

export function LinearSurface() {
  return <PixelDataTable<Row> data={rows} columns={columns} surface="linear" />;
}

Sortable columns

import * as React from 'react';
import { PixelDataTable, createColumnHelper, type ColumnDef } from '@pxlkit/ui-kit';

type Row = {
  id: string;
  name: string;
  role: string;
  status: string;
};

const rows: Row[] = [
  { id: '1', name: 'Alice', role: 'Engineer', status: 'active' },
  { id: '2', name: 'Bob', role: 'Designer', status: 'idle' },
  { id: '3', name: 'Carol', role: 'PM', status: 'active' },
  { id: '4', name: 'Dan', role: 'Engineer', status: 'active' },
  { id: '5', name: 'Eve', role: 'Designer', status: 'idle' },
];

const ch = createColumnHelper<Row>();

const columns: ColumnDef<Row, unknown>[] = [
  ch.accessor('name', { header: 'Name' }) as ColumnDef<Row, unknown>,
  ch.accessor('role', { header: 'Role' }) as ColumnDef<Row, unknown>,
  ch.accessor('status', { header: 'Status' }) as ColumnDef<Row, unknown>,
];

export function Sortable() {
  const [sorting, setSorting] = React.useState<{ id: string; desc: boolean }[]>([
    { id: 'name', desc: false },
  ]);
  return (
    <PixelDataTable<Row>
      data={rows}
      columns={columns}
      sorting={sorting}
      onSortingChange={setSorting}
    />
  );
}

Row selection

import * as React from 'react';
import { PixelDataTable, createColumnHelper, type ColumnDef } from '@pxlkit/ui-kit';

type Row = {
  id: string;
  name: string;
  role: string;
  status: string;
};

const rows: Row[] = [
  { id: '1', name: 'Alice', role: 'Engineer', status: 'active' },
  { id: '2', name: 'Bob', role: 'Designer', status: 'idle' },
  { id: '3', name: 'Carol', role: 'PM', status: 'active' },
  { id: '4', name: 'Dan', role: 'Engineer', status: 'active' },
  { id: '5', name: 'Eve', role: 'Designer', status: 'idle' },
];

const ch = createColumnHelper<Row>();

const columns: ColumnDef<Row, unknown>[] = [
  ch.accessor('name', { header: 'Name' }) as ColumnDef<Row, unknown>,
  ch.accessor('role', { header: 'Role' }) as ColumnDef<Row, unknown>,
  ch.accessor('status', { header: 'Status' }) as ColumnDef<Row, unknown>,
];

export function RowSelection() {
  const [selection, setSelection] = React.useState<Record<string, boolean>>({});
  return (
    <PixelDataTable<Row>
      data={rows}
      columns={columns}
      rowSelection={selection}
      onRowSelectionChange={setSelection}
    />
  );
}

Pagination

import * as React from 'react';
import { PixelDataTable, createColumnHelper, type ColumnDef } from '@pxlkit/ui-kit';

type Row = {
  id: string;
  name: string;
  role: string;
  status: string;
};

const rows: Row[] = [
  { id: '1', name: 'Alice', role: 'Engineer', status: 'active' },
  { id: '2', name: 'Bob', role: 'Designer', status: 'idle' },
  { id: '3', name: 'Carol', role: 'PM', status: 'active' },
  { id: '4', name: 'Dan', role: 'Engineer', status: 'active' },
  { id: '5', name: 'Eve', role: 'Designer', status: 'idle' },
];

const ch = createColumnHelper<Row>();

const columns: ColumnDef<Row, unknown>[] = [
  ch.accessor('name', { header: 'Name' }) as ColumnDef<Row, unknown>,
  ch.accessor('role', { header: 'Role' }) as ColumnDef<Row, unknown>,
  ch.accessor('status', { header: 'Status' }) as ColumnDef<Row, unknown>,
];

export function Pagination() {
  const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: 2 });
  return (
    <PixelDataTable<Row>
      data={rows}
      columns={columns}
      pagination={pagination}
      onPaginationChange={setPagination}
    />
  );
}

Compact density

import { PixelDataTable, createColumnHelper, type ColumnDef } from '@pxlkit/ui-kit';

type Row = {
  id: string;
  name: string;
  role: string;
  status: string;
};

const rows: Row[] = [
  { id: '1', name: 'Alice', role: 'Engineer', status: 'active' },
  { id: '2', name: 'Bob', role: 'Designer', status: 'idle' },
  { id: '3', name: 'Carol', role: 'PM', status: 'active' },
  { id: '4', name: 'Dan', role: 'Engineer', status: 'active' },
  { id: '5', name: 'Eve', role: 'Designer', status: 'idle' },
];

const ch = createColumnHelper<Row>();

const columns: ColumnDef<Row, unknown>[] = [
  ch.accessor('name', { header: 'Name' }) as ColumnDef<Row, unknown>,
  ch.accessor('role', { header: 'Role' }) as ColumnDef<Row, unknown>,
  ch.accessor('status', { header: 'Status' }) as ColumnDef<Row, unknown>,
];

export function CompactDensity() {
  return <PixelDataTable<Row> data={rows} columns={columns} density="compact" />;
}

Comfortable density

import { PixelDataTable, createColumnHelper, type ColumnDef } from '@pxlkit/ui-kit';

type Row = {
  id: string;
  name: string;
  role: string;
  status: string;
};

const rows: Row[] = [
  { id: '1', name: 'Alice', role: 'Engineer', status: 'active' },
  { id: '2', name: 'Bob', role: 'Designer', status: 'idle' },
  { id: '3', name: 'Carol', role: 'PM', status: 'active' },
  { id: '4', name: 'Dan', role: 'Engineer', status: 'active' },
  { id: '5', name: 'Eve', role: 'Designer', status: 'idle' },
];

const ch = createColumnHelper<Row>();

const columns: ColumnDef<Row, unknown>[] = [
  ch.accessor('name', { header: 'Name' }) as ColumnDef<Row, unknown>,
  ch.accessor('role', { header: 'Role' }) as ColumnDef<Row, unknown>,
  ch.accessor('status', { header: 'Status' }) as ColumnDef<Row, unknown>,
];

export function ComfortableDensity() {
  return <PixelDataTable<Row> data={rows} columns={columns} density="comfortable" />;
}

Loading

import { PixelDataTable, createColumnHelper, type ColumnDef } from '@pxlkit/ui-kit';

type Row = {
  id: string;
  name: string;
  role: string;
  status: string;
};

const ch = createColumnHelper<Row>();

const columns: ColumnDef<Row, unknown>[] = [
  ch.accessor('name', { header: 'Name' }) as ColumnDef<Row, unknown>,
  ch.accessor('role', { header: 'Role' }) as ColumnDef<Row, unknown>,
  ch.accessor('status', { header: 'Status' }) as ColumnDef<Row, unknown>,
];

export function Loading() {
  return <PixelDataTable<Row> data={[]} columns={columns} loading />;
}

Empty state

import { PixelDataTable, createColumnHelper, type ColumnDef } from '@pxlkit/ui-kit';

type Row = {
  id: string;
  name: string;
  role: string;
  status: string;
};

const ch = createColumnHelper<Row>();

const columns: ColumnDef<Row, unknown>[] = [
  ch.accessor('name', { header: 'Name' }) as ColumnDef<Row, unknown>,
  ch.accessor('role', { header: 'Role' }) as ColumnDef<Row, unknown>,
  ch.accessor('status', { header: 'Status' }) as ColumnDef<Row, unknown>,
];

export function Empty() {
  return (
    <PixelDataTable<Row>
      data={[]}
      columns={columns}
      emptyState={<span>No records found.</span>}
    />
  );
}

Sticky header

import { PixelDataTable, createColumnHelper, type ColumnDef } from '@pxlkit/ui-kit';

type Row = {
  id: string;
  name: string;
  role: string;
  status: string;
};

const rows: Row[] = [
  { id: '1', name: 'Alice', role: 'Engineer', status: 'active' },
  { id: '2', name: 'Bob', role: 'Designer', status: 'idle' },
  { id: '3', name: 'Carol', role: 'PM', status: 'active' },
  { id: '4', name: 'Dan', role: 'Engineer', status: 'active' },
  { id: '5', name: 'Eve', role: 'Designer', status: 'idle' },
];

const ch = createColumnHelper<Row>();

const columns: ColumnDef<Row, unknown>[] = [
  ch.accessor('name', { header: 'Name' }) as ColumnDef<Row, unknown>,
  ch.accessor('role', { header: 'Role' }) as ColumnDef<Row, unknown>,
  ch.accessor('status', { header: 'Status' }) as ColumnDef<Row, unknown>,
];

export function StickyHeader() {
  return <PixelDataTable<Row> data={rows} columns={columns} stickyHeader />;
}

Clickable rows

import * as React from 'react';
import { PixelDataTable, createColumnHelper, type ColumnDef } from '@pxlkit/ui-kit';

type Row = {
  id: string;
  name: string;
  role: string;
  status: string;
};

const rows: Row[] = [
  { id: '1', name: 'Alice', role: 'Engineer', status: 'active' },
  { id: '2', name: 'Bob', role: 'Designer', status: 'idle' },
  { id: '3', name: 'Carol', role: 'PM', status: 'active' },
  { id: '4', name: 'Dan', role: 'Engineer', status: 'active' },
  { id: '5', name: 'Eve', role: 'Designer', status: 'idle' },
];

const ch = createColumnHelper<Row>();

const columns: ColumnDef<Row, unknown>[] = [
  ch.accessor('name', { header: 'Name' }) as ColumnDef<Row, unknown>,
  ch.accessor('role', { header: 'Role' }) as ColumnDef<Row, unknown>,
  ch.accessor('status', { header: 'Status' }) as ColumnDef<Row, unknown>,
];

export function ClickableRows() {
  const [last, setLast] = React.useState<string>('');
  return (
    <div>
      <PixelDataTable<Row>
        data={rows}
        columns={columns}
        onRowClick={(row) => setLast(row.name)}
      />
      <p>Last clicked: {last || 'none'}</p>
    </div>
  );
}

Related