Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

DataTable

A virtualized data table — renders any number of rows via @tanstack/react-virtual, density-aware and sortable. Columns reorder by dragging a per-header grip handle, hide/show from a "Columns" menu, and the resulting layout can persist to localStorage.

Import

DataTable is a registry composite — you own the source after installing:

npx ploxum add data-table
import { DataTable, type Column } from "@/components/data-table";

Depends on @roger-emerson/ploxum-primitives (Checkbox, DropdownMenu, useDensity), @roger-emerson/ploxum-utils (cn, usePrefersReducedMotion), @tanstack/react-virtual, and motion (drag-to-reorder).

Usage

interface Gate {
  id: string;
  label: string;
  scans: number;
  perMin: number;
  status: "ok" | "warning";
}
 
const columns: Column<Gate>[] = [
  { key: "label", label: "Gate", sortable: true },
  { key: "scans", label: "Scans", align: "right", mono: true, sortable: true },
  { key: "perMin", label: "Per minute", align: "right", mono: true, sortable: true },
  {
    key: "status",
    label: "Status",
    render: (r) => <Badge tone={r.status === "warning" ? "warning" : "success"}>{r.status}</Badge>,
  },
];
 
<DataTable
  rows={gates}
  columns={columns}
  getRowId={(r) => r.id}
  caption="Traffic & Guests Flow"
  initialSort={{ key: "scans", dir: "desc" }}
  persistKey="gates-flow"
/>;

Sorting is unchanged from before: click a sortable header's label to cycle asc → desc → none. Reordering is a separate gesture — drag the grip handle that appears on the left of each header on hover or keyboard focus — so a drag never triggers a sort. Both behaviors coexist with row virtualization.

Column reordering

Each header carries a grip handle bound to a motion Reorder.Item via useDragControls (handle-only drag). Drop commits the new order. Leave columnOrder / onColumnOrderChange unset for uncontrolled behavior, or pass them to drive order from your own state:

const [order, setOrder] = useState(["status", "label", "scans", "perMin"]);
 
<DataTable
  rows={gates}
  columns={columns}
  getRowId={(r) => r.id}
  columnOrder={order}
  onColumnOrderChange={setOrder}
/>;

The reorder spring honors prefers-reduced-motion — under reduce, the transition is instant.

Column visibility

A Columns control sits top-right (a DropdownMenu of Checkbox rows showing visible/total). Uncontrolled by default; pass columnVisibility ({ [key]: boolean }, omitted/true = shown, false = hidden) + onColumnVisibilityChange to control it. The last visible column cannot be hidden.

Persisted layout

Set persistKey to persist column order + visibility to localStorage under ploxum:data-table:<persistKey>. The layout is read once on mount and re-written on every reorder/toggle. All access is wrapped in try/catch, so private-mode or quota failures degrade silently. Stored keys are reconciled against the live columns — unknown keys are dropped and newly-added columns append in their declared position.

Props

DataTable

PropTypeDefaultDescription
rowsT[]Row data.
columnsColumn<T>[]Column definitions, in default order.
getRowId(row: T) => stringStable row key.
captionstringHeader strip label above the table.
heightnumber480Height of the virtualized scroll container in px.
initialSort{ key: string; dir: "asc" | "desc" } | nullnullInitial sort state.
virtualizeThresholdnumber24Skip virtualization below this row count.
columnOrderstring[]Controlled order (column keys). Falls back to internal state.
onColumnOrderChange(order: string[]) => voidFires on drag-drop with the reconciled order.
columnVisibilityRecord<string, boolean>Controlled visibility map. Falls back to internal state.
onColumnVisibilityChange(visibility: Record<string, boolean>) => voidFires on a Columns toggle.
persistKeystringWhen set, order + visibility persist to localStorage.

Column<T>

FieldTypeDescription
keykeyof T & stringField key; also the column's identity for ordering / visibility.
labelstringHeader text.
align"left" | "right"Cell + header alignment. Defaults to left.
monoboolean?Render cells font-mono tabular-nums.
sortableboolean?Enable sort-on-click for the header label.
render(row: T) => ReactNodeCustom cell renderer; falls back to String(row[key]).
widthnumber?Fixed column width in px.

Design rules

  • Drag vs. sort are distinct gestures. Reorder is bound to a dedicated grip handle (handle-only dragControls), so clicking a sortable header label always sorts and never starts a drag.
  • The reorder animation is a spring, but collapses to an instant transition under prefers-reduced-motion (P1 rule).
  • Numbers stay tabular-nums (via mono) so columns don't reflow while sorting or reordering.
  • Tokens only — every color is a var(--ploxum-color-*); no raw hex or shadcn utility classes.
  • The Columns menu refuses to hide the final visible column, so the table never renders zero columns.

Storybook

Composites/DataTable ↗