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)
| Atlas | Usman Farooqi | $24,000 |
| North Star | Maya Chen | $18,500 |
| Harbor Analytics | Jordan Lee | $9,250 |
| Beacon | Priya 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
| Prop | Type | Default | Description |
|---|---|---|---|
| DataTableSortHeader.sortDirection | "ascending" | "descending" | "none" | — | This column's current sort state — typically from useDataTableSort's getSortDirection(column). |
| DataTableSortHeader.onSort | () => void | — | Called when the header is activated. Typically calls useDataTableSort's toggleSort(column). |
| DataTableSortHeader.disabled | boolean | false | Disables 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}
/>
</>
);
}