Skip to content
Protocore Design Systemv1.6.9

/// 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";
View as Markdown

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.

EventKindSourceStatus
evt_7f21ledger.settledsettle-workerok
evt_7f20fx.quotedfx-gatewayretry
evt_7f1fwebhook.sentnotifierdropped

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.

10 transactions
TransactionMethod
acct_eu_00417sepasettled+€128.40
acct_eu_00418cardpending+€4,120.00
acct_eu_00419separeversed−€980.00
acct_eu_00417wiresettled+€42,500.00
Page 1 of 3 · 10 rows

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.

PayoutDestinationAmount
po_3391acct_eu_00417€1,204.00
po_3390acct_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

KeysAction
TabMoves 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.
EnterOpens the focused row (fires `onRowClick`).
SpaceToggles 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

PropTypeDefaultDescription
barebooleanfalseDrop the outer border + surface so the table sits flush inside another frame.
classNamestringExtra 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).
emptySlotReactNodePurposeful empty content shown when there are zero rows.
errorDataTableError | nullnullFailure 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.
loadingbooleanfalseShow skeleton rows instead of data. Loading wins over `error`.
minWidthnumber960The 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).
paginationDataTablePaginationPagination footer wiring — omit to hide the footer.
pinFirstColumnbooleantrueFreeze the first data column while the body scrolls horizontally.
selectionDataTableSelectionRow-selection wiring — adds a checkbox column and the selection bar.
skeletonRowsnumber6How many skeleton rows to draw while `loading`.
sortingSortingStateServer-driven sort state — the component never sorts client-side.
summaryReactNodeToolbar left side — a count summary (e.g. "1,328 events"). Use `<b>` for emphasised figures.
toolbarActionsReactNodeToolbar right side — filters, refresh, export, etc.

Related

  • TableThe styled table primitive — thin compound wrappers over the native table elements that carry the console look.
  • PaginationPage navigation — a mono button row. Numbered mode draws pages with ellipses and an inverted current page; cursor mode draws prev/next only.
  • EmptyStateA centered, dashed-frame placeholder for a surface that legitimately has nothing to show.
  • BadgeA static, sentence-case status indicator with a tone-tinted fill — never interactive, never color-only.