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-tableimport { 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
| Prop | Type | Default | Description |
|---|---|---|---|
rows | T[] | — | Row data. |
columns | Column<T>[] | — | Column definitions, in default order. |
getRowId | (row: T) => string | — | Stable row key. |
caption | string | — | Header strip label above the table. |
height | number | 480 | Height of the virtualized scroll container in px. |
initialSort | { key: string; dir: "asc" | "desc" } | null | null | Initial sort state. |
virtualizeThreshold | number | 24 | Skip virtualization below this row count. |
columnOrder | string[] | — | Controlled order (column keys). Falls back to internal state. |
onColumnOrderChange | (order: string[]) => void | — | Fires on drag-drop with the reconciled order. |
columnVisibility | Record<string, boolean> | — | Controlled visibility map. Falls back to internal state. |
onColumnVisibilityChange | (visibility: Record<string, boolean>) => void | — | Fires on a Columns toggle. |
persistKey | string | — | When set, order + visibility persist to localStorage. |
Column<T>
| Field | Type | Description |
|---|---|---|
key | keyof T & string | Field key; also the column's identity for ordering / visibility. |
label | string | Header text. |
align | "left" | "right" | Cell + header alignment. Defaults to left. |
mono | boolean? | Render cells font-mono tabular-nums. |
sortable | boolean? | Enable sort-on-click for the header label. |
render | (row: T) => ReactNode | Custom cell renderer; falls back to String(row[key]). |
width | number? | 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(viamono) 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.
