PixelTable

Generic data table with striped rows, hover highlight, controlled sorting, single/multi row selection, sticky headers, density, loading skeletons and empty state.

  • Controlled sort with header buttons + aria-sort semantics
  • Single or multi-row selection with checkbox column and indeterminate state
  • Sticky header and sticky first column for wide datasets
  • Built-in skeleton loading rows and configurable empty state
  • Three density presets (compact, normal, comfortable) on pixel or linear surface
Status
stable
Category
data
Since
v1.0.0

API

import { PixelTable } from '@pxlkit/ui-kit';
PropTypeDefaultDescription
columns (required)Array<PixelTableColumn<Row>>—Column definitions.
data (required)Row[]—Row data.
stripedbooleantrueAlternate-row tint. Defaults to true.
surface'pixel' | 'linear'—Visual surface override.
sortPixelTableSortState—Controlled sort state.
onSortChange(next: PixelTableSortState) => void—Called when a sortable header is clicked.
selection'single' | 'multi'—Enables a leading checkbox column with single- or multi-row selection.
selectedIdsstring[]—Controlled list of selected row ids.
onSelectionChange(next: string[]) => void—Called when selection changes.
getRowId(row: Row, idx: number) => string—Resolves a stable id per row. Falls back to row.id then to the index.
stickyHeaderboolean—Sticks the header row to the top of the scroll container.
stickyFirstColumnboolean—Sticks the first column to the left of the scroll container.
loadingboolean—Renders skeleton rows instead of data.
emptyStateReact.ReactNode—Rendered inside a full-width cell when data is empty.
onRowClick(row: Row, idx: number) => void—Called when a body row is clicked, or on Enter / Space on it. Renders it focusable, with cursor-pointer.
density'compact' | 'normal' | 'comfortable''normal'Cell padding scale. Defaults to 'normal'.
borderedbooleantrueRender with surface-aware border + radius chrome. Defaults to true — tables need visible chrome.

Accessibility

WCAG target: 2.1 AA

  • table

Uses native <table>, <thead>, <tbody>, <th scope="col"> and aria-sort on sortable columns. Loading state exposes role="status" with aria-live="polite". The multi-select header checkbox reflects indeterminate state when only some rows are selected.

Keyboard

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

Usage

import { PixelTable } 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' },
];

const baseColumns = [
  { key: 'name', header: 'Name' },
  { key: 'role', header: 'Role' },
  { key: 'status', header: 'Status' },
];

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

Examples

Default

import { PixelTable } 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' },
];

const baseColumns = [
  { key: 'name', header: 'Name' },
  { key: 'role', header: 'Role' },
  { key: 'status', header: 'Status' },
];

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

Striped

import { PixelTable } 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' },
];

const baseColumns = [
  { key: 'name', header: 'Name' },
  { key: 'role', header: 'Role' },
  { key: 'status', header: 'Status' },
];

export function Striped() {
  return <PixelTable<Row> columns={baseColumns} data={rows} striped />;
}

Pixel surface

import { PixelTable } 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' },
];

const baseColumns = [
  { key: 'name', header: 'Name' },
  { key: 'role', header: 'Role' },
  { key: 'status', header: 'Status' },
];

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

Linear surface

import { PixelTable } 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' },
];

const baseColumns = [
  { key: 'name', header: 'Name' },
  { key: 'role', header: 'Role' },
  { key: 'status', header: 'Status' },
];

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

Sortable columns

import * as React from 'react';
import { PixelTable, type PixelTableSortState } 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' },
];

export function Sortable() {
  const [sort, setSort] = React.useState<PixelTableSortState>({ key: 'name', dir: 'asc' });
  const sortable = [
    { key: 'name', header: 'Name', sortable: true },
    { key: 'role', header: 'Role', sortable: true },
    { key: 'status', header: 'Status' },
  ];
  return (
    <PixelTable<Row>
      columns={sortable}
      data={rows}
      sort={sort}
      onSortChange={setSort}
    />
  );
}

Single selection

import * as React from 'react';
import { PixelTable } 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' },
];

const baseColumns = [
  { key: 'name', header: 'Name' },
  { key: 'role', header: 'Role' },
  { key: 'status', header: 'Status' },
];

export function SingleSelection() {
  const [selected, setSelected] = React.useState<string[]>(['1']);
  return (
    <PixelTable<Row>
      columns={baseColumns}
      data={rows}
      selection="single"
      selectedIds={selected}
      onSelectionChange={setSelected}
    />
  );
}

Multi selection

import * as React from 'react';
import { PixelTable } 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' },
];

const baseColumns = [
  { key: 'name', header: 'Name' },
  { key: 'role', header: 'Role' },
  { key: 'status', header: 'Status' },
];

export function MultiSelection() {
  const [selected, setSelected] = React.useState<string[]>([]);
  return (
    <PixelTable<Row>
      columns={baseColumns}
      data={rows}
      selection="multi"
      selectedIds={selected}
      onSelectionChange={setSelected}
    />
  );
}

Compact density

import { PixelTable } 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' },
];

const baseColumns = [
  { key: 'name', header: 'Name' },
  { key: 'role', header: 'Role' },
  { key: 'status', header: 'Status' },
];

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

Comfortable density

import { PixelTable } 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' },
];

const baseColumns = [
  { key: 'name', header: 'Name' },
  { key: 'role', header: 'Role' },
  { key: 'status', header: 'Status' },
];

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

Loading

import { PixelTable } from '@pxlkit/ui-kit';

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

const baseColumns = [
  { key: 'name', header: 'Name' },
  { key: 'role', header: 'Role' },
  { key: 'status', header: 'Status' },
];

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

Empty state

import { PixelTable } from '@pxlkit/ui-kit';

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

const baseColumns = [
  { key: 'name', header: 'Name' },
  { key: 'role', header: 'Role' },
  { key: 'status', header: 'Status' },
];

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

Sticky header

import { PixelTable } 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' },
];

const baseColumns = [
  { key: 'name', header: 'Name' },
  { key: 'role', header: 'Role' },
  { key: 'status', header: 'Status' },
];

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

Custom cell render

import { PixelTable } 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' },
];

export function CustomRender() {
  const columns = [
    { key: 'name', header: 'Name' },
    {
      key: 'status',
      header: 'Status',
      render: (row: Row) => (
        <span style={{ textTransform: 'uppercase' }}>{row.status}</span>
      ),
    },
  ];
  return <PixelTable<Row> columns={columns} data={rows} />;
}

Clickable rows

import * as React from 'react';
import { PixelTable } 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' },
];

const baseColumns = [
  { key: 'name', header: 'Name' },
  { key: 'role', header: 'Role' },
  { key: 'status', header: 'Status' },
];

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

Related