skrewww

Content & Data

Data Table

Data Table is an interactive data-table pattern composing Table with header sorting and external Pagination — no columns-config prop; the consumer writes their own Table/TableHead/TableBody markup and drops in DataTableSortHeader for sortable columns.

Sortable header building block — no density/selection/sticky variants in Beta

betaReact AvailableFigma UnavailableDocs Partial
React last updated
2026-07-15
Documentation last updated
2026-07-15
Accessibility target
WCAG 2.2 AA (target)
Version
0.1.0-beta

Known open questions

  • No Figma component set exists for Data Table yet — sort-header anatomy and token bindings are React-first, same precedent as Table.
  • Row selection (Checkbox column), sticky headers, density variants, and virtualization remain deferred to a later pass — not in this MVP.
  • Row-actions conventions beyond composing Menu in a cell are still open.

Sort + external Pagination

DataTableSortHeader replaces TableHead on sortable columns. useDataTableSort tracks which column and direction; Pagination composes separately, driven by the same page state the consumer already owns.

Projects (client-side sorted and paginated)

Projects
AtlasUsman Farooqi$24,000
North StarMaya Chen$18,500
Harbor AnalyticsJordan Lee$9,250
BeaconPriya Nair$31,200

Purpose

The interactive data-table pattern: composes Table with a sortable column-header building block (DataTableSortHeader) and a sort-state hook (useDataTableSort), plus external Pagination composition — sorting only, no columns-config prop.

Anatomy

Data Table is a pattern, not a wrapper component: compose Table/TableScrollArea/TableCaption/TableHeader/TableBody as usual, and use DataTableSortHeader in place of TableHead for sortable columns. useDataTableSort manages which column is sorted and in which direction. Pagination composes alongside, outside Table, with no embedded page API.

Variants and states

sortable-header

When to use

Tabular data that needs single-column sorting on top of Table's presentational foundation — the consumer still writes real Table/TableHead/TableBody markup and drops DataTableSortHeader in for sortable columns.

When not to use

Read-only tabular data with no sort interaction — use plain Table. Row selection, spreadsheet-style cell editing, or arrow-key cell navigation — all explicitly out of scope; Data Table deliberately avoids role="grid".

Accessibility

DataTableSortHeader renders a real <button> inside <th> with aria-sort set to "ascending", "descending", or "none". Native button semantics give Enter/Space activation and normal Tab focus for free — no custom keyboard handling. Table's own accessibility rules (native semantics, no role="grid", consumer-supplied scope) are unchanged.

Keyboard behavior

DataTableSortHeader renders a real button — Tab reaches it as a normal focusable control, Enter and Space activate it like any button. No composite/grid keyboard model; arrow keys are not captured. Table's own keyboard rules (no role="grid", tabIndex on TableScrollArea consumer-controlled) are unchanged.

Common mistakes

Expecting a columns/rows prop that generates markup — Data Table has none, by design; building row selection, sticky headers, density variants, or virtualization into this MVP — all explicitly deferred; embedding pagination inside Data Table instead of composing the separate Pagination component; using a non-button element as the sort trigger.

Properties

DataTableSortHeader: sortDirection ("ascending"|"descending"|"none"), onSort, disabled, plus all TableHead props (scope, align, etc.). useDataTableSort(options): sortState/defaultSortState/onSortStateChange (dual controlled/uncontrolled), returns { sortState, getSortDirection, toggleSort }. Sort cycle per column: none -> ascending -> descending -> none; a different column always resets to ascending.

What is the difference between Table and Data Table?

Table is the presentational foundation — captions, headers, rows, cells, overflow. Data Table is the interaction pattern: it composes Table and adds a sortable header building block (DataTableSortHeader) plus a sort-state hook (useDataTableSort). Data Table does not fork Table's markup or add its own role.

Why doesn't Data Table take a columns prop?

Data Table deliberately has no columns-config API. The consumer still writes real Table/TableHead/TableBody markup — Data Table only supplies the sortable header building block and the sort-state hook on top of markup the consumer already owns, keeping Table's audited semantics (native scope, RTL, TableScrollArea) as the single source of truth.

Is Data Table's sort state controlled or uncontrolled?

Both — useDataTableSort uses the same useControllableState hook as Accordion, Dialog, Drawer, and CalendarGrid's range mode. Pass sortState + onSortStateChange for controlled usage, or defaultSortState for uncontrolled usage; omit both for a fully internal default.

What is the sort cycle?

Per column: none -> ascending -> descending -> none. Activating a different column always resets it to ascending and clears the previous column's sort — only one column sorts at a time in this MVP.

Does Data Table support row selection?

Not in v1 — explicitly deferred. Consumers may still compose Checkbox inside a TableCell manually, the same way they can with plain Table, but Data Table has no selection API of its own.

How does pagination work with Data Table?

External composition only — render the existing Pagination component alongside your Table, driving it from your own current-page state. Data Table has no embedded or compound pagination API.

Tokens used

semantic/surface/defaultsemantic/surface/elevatedsemantic/border/defaultsemantic/text/primarysemantic/icon/mutedsemantic/focus-ring

Known limitation

React-first MVP implemented 2026-07-15 — no Figma component set exists yet for Data Table specifically. Row selection, sticky headers, density variants, and virtualization remain deferred to a later pass. See docs/architecture/data-table-discovery.md.

Component API

PropTypeDefaultDescription
DataTableSortHeader.sortDirection"ascending" | "descending" | "none"This column's current sort state — typically from useDataTableSort's getSortDirection(column).
DataTableSortHeader.onSort() => voidCalled when the header is activated. Typically calls useDataTableSort's toggleSort(column).
DataTableSortHeader.disabledbooleanfalseDisables the sort button for this column.
useDataTableSort(options){ sortState?, defaultSortState?, onSortStateChange? }Dual controlled/uncontrolled sort-state hook. Returns { sortState, getSortDirection, toggleSort }.

React example

Copy React example
import { useState } from "react";
import {
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableHeader,
  TableRow,
  TableScrollArea,
  DataTableSortHeader,
} from "@/components/ui";
import { useDataTableSort } from "@/lib/use-data-table-sort";
import { Pagination, buildPaginationItems } from "@/components/ui";

const projects = [
  { id: "atlas", name: "Atlas", budget: 24000 },
  { id: "north-star", name: "North Star", budget: 18500 },
  { id: "harbor", name: "Harbor Analytics", budget: 9250 },
];

export function Example() {
  const { sortState, getSortDirection, toggleSort } = useDataTableSort<"name" | "budget">();
  const [page, setPage] = useState(1);

  const sorted = [...projects].sort((a, b) => {
    if (!sortState.column) return 0;
    const factor = sortState.direction === "ascending" ? 1 : -1;
    return a[sortState.column] > b[sortState.column] ? factor : -factor;
  });

  return (
    <>
      <TableScrollArea accessibleLabel="Scrollable projects table">
        <Table>
          <TableCaption>Projects</TableCaption>
          <TableHeader>
            <TableRow>
              <DataTableSortHeader
                scope="col"
                sortDirection={getSortDirection("name")}
                onSort={() => toggleSort("name")}
              >
                Project
              </DataTableSortHeader>
              <DataTableSortHeader
                scope="col"
                align="end"
                sortDirection={getSortDirection("budget")}
                onSort={() => toggleSort("budget")}
              >
                Budget
              </DataTableSortHeader>
            </TableRow>
          </TableHeader>
          <TableBody>
            {sorted.map((project) => (
              <TableRow key={project.id}>
                <TableCell>{project.name}</TableCell>
                <TableCell align="end">${project.budget.toLocaleString()}</TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </TableScrollArea>
      <Pagination
        items={buildPaginationItems({ currentPage: page, totalPages: 3 })}
        onPageChange={setPage}
      />
    </>
  );
}