/// Data Display
DataTable
The server-driven console workhorse — TanStack v8 in manual mode with sorting, pagination, selection, and five body states.
import { DataTable } from "@protocore/pds";Basics
At its floor DataTable takes just columns (TanStack column defs) and data. You get the console-styled grid — sticky sunken header, pinned first column, horizontal scroll — plus its built-in body states, with no sorting or pagination wired.
| Event | Kind | Source | Status |
|---|---|---|---|
| evt_7f21 | ledger.settled | settle-worker | ok |
| evt_7f20 | fx.quoted | fx-gateway | retry |
| evt_7f1f | webhook.sent | notifier | dropped |
Server-driven console
The flagship pattern. DataTable is fully manual: it never sorts or paginates client-side — it reports *intent* (onSortingChange, pagination.onNext/onPrev) and you feed back the page the server returned. Here an in-memory slice stands in for that server. Row selection is keyed by getRowId, and the selection bar hosts capability-gated bulk actions. Click a header to cycle none → descending → ascending; select rows to reveal the bar.
| Transaction | Method | ||||
|---|---|---|---|---|---|
| acct_eu_00417 | sepa | settled | +€128.40 | ||
| acct_eu_00418 | card | pending | +€4,120.00 | ||
| acct_eu_00419 | sepa | reversed | −€980.00 | ||
| acct_eu_00417 | wire | settled | +€42,500.00 |
The five body states
One component owns the whole lifecycle: skeleton loading rows, a purposeful empty slot, an error panel carrying a copyable debugId and a Retry, and the data state. Loading always wins over error, so a retry visibly transitions error → skeletons → data.
| Payout | Destination | Amount |
|---|---|---|
| po_3391 | acct_eu_00417 | €1,204.00 |
| po_3390 | acct_eu_00418 | €57.80 |
When to use it
Reach for DataTable for every list screen backed by a server: transactions, events, API keys, audit logs — anything paginated, sortable, selectable, or fetched. It bakes in the console doctrine so you don't re-implement it per screen: the none→desc→asc sort cycle, roving-tabindex keyboard navigation, the selection bar, and the loading/empty/error/data states with a copyable debug id.
Use the lower-level Table primitive instead for small, static, hand-rendered grids you fully control. Use DefinitionList for a single entity's fields (key→value), not a two-column table. Because sorting and pagination are manual, DataTable pairs naturally with your data layer's query state — the component holds *no* opinion about how you fetch.
Selection needs a stable row id
Whenever you pass selection, also pass getRowId. Without it, selection keys by page-local index, so flipping the page silently re-targets the current selection onto different rows (the component warns about this in development). A stable id from your data — a transaction id, a key id — keeps selection correct across pages.
Usage
Do
- Drive `sorting` and `pagination` from your query state — DataTable reports intent, the server returns the page.
- Always pair `selection` with `getRowId` so selection survives page changes.
- Give the error state a `debugId` so operators can copy it straight into a support ticket.
- Mark amount columns `meta.numeric` so they right-align in tabular mono.
Don't
- Don't expect client-side sorting or paging — the component is manual by design.
- Don't use it for a fixed handful of static rows; that's the Table primitive.
- Don't gate bulk actions in the UI only — the selection bar renders whatever you pass, so capability-check the actions themselves.
- Don't omit `getRowId` when selecting; index-keyed selection breaks on a page flip.
Accessibility
| Keys | Action |
|---|---|
| Tab | Moves into the table; with `onRowClick` set, a single row is a roving tabindex stop. |
| ↑ / ↓ | Moves the active row up/down (wraps at the ends) when row navigation is enabled. |
| Enter | Opens the focused row (fires `onRowClick`). |
| Space | Toggles the row's selection checkbox when focus is on it. |
| Enter / Space (header) | Activates a sortable header, cycling none → descending → ascending. |
- Sortable headers expose `aria-sort` (`none`/`ascending`/`descending`) so screen readers announce the current sort.
- The table sets `aria-busy` while loading; the error panel is a `role="alert"` region.
- The header select-all checkbox reflects an indeterminate state as a DOM property when only some rows are selected.
- In-row controls keep their own keyboard behaviour — row navigation only acts when the row element itself holds focus.
DataTable props
| Prop | Type | Default | Description |
|---|---|---|---|
bare | boolean | false | Drop the outer border + surface so the table sits flush inside another frame. |
className | string | — | Extra className on the outer container. |
columns * | ColumnDef<TData, unknown>[] | — | TanStack column definitions. Use `meta.numeric` for amount columns; display columns need no accessor. |
data * | TData[] | — | The current page of rows (already fetched by the server). |
emptySlot | ReactNode | — | Purposeful empty content shown when there are zero rows. |
error | DataTableError | null | null | Failure payload — renders the error panel (with retry + debugId) below the header. |
getRowId | ((row: TData, index: number) => string) | — | Stable row identity — required whenever `selection` is used. |
loading | boolean | false | Show skeleton rows instead of data. Loading wins over `error`. |
minWidth | number | 960 | The table's natural min width; the panel scrolls horizontally below it. |
onRowClick | ((row: TData) => void) | — | Open a row. Enables keyboard row navigation (roving tabindex, ↑/↓ wrap, Enter). |
onSortingChange | ((next: SortingState) => void) | — | Called with the next sort state on a header click (none → desc → asc → none). |
pagination | DataTablePagination | — | Pagination footer wiring — omit to hide the footer. |
pinFirstColumn | boolean | true | Freeze the first data column while the body scrolls horizontally. |
selection | DataTableSelection | — | Row-selection wiring — adds a checkbox column and the selection bar. |
skeletonRows | number | 6 | How many skeleton rows to draw while `loading`. |
sorting | SortingState | — | Server-driven sort state — the component never sorts client-side. |
summary | ReactNode | — | Toolbar left side — a count summary (e.g. "1,328 events"). Use `<b>` for emphasised figures. |
toolbarActions | ReactNode | — | Toolbar right side — filters, refresh, export, etc. |