Skip to content

Table / data grid

In review
Live · web
Density
1 selected
Sessions with status, token usage, cost, and last-updated time. Column headers sort; the first column selects rows.
nockerl-design · docs siteStreaming137,730$2.41now
api-server · gateway refactorIdle89,210$1.5812m
credential-store · allowlist auditNeeds attention41,980$0.741h
dueydo · failed deployFailed15,240$0.293h
Total · 5 sessions348,710$6.14
Showing 1 to 4 of 5

Loading: skeleton rows hold the column grid

SessionTokensCost

Sorted by updated (asc) · 1 selected · page 1 of 2 · comfortable. The grid is live: change --color-divider and every hairline moves.

// Web: React, consuming @dizyx/nockerl-tokens. NockerlTable ships from
// @dizyx/nockerl-react: the typed columns, sortable headers, checkbox selection
// (select-all + per-row), sticky header, footer summary, and loading / empty
// states are all in the package. v1 ships columns · sort · sticky · selection ·
// empty · loading · density · footer; row virtualization + inline cell editing
// are a later slice. (For a single-column sectioned list use NockerlListItem; for
// one record's fields use Key-value.) NockerlTable is CONTROLLED. The caller
// sorts `rows` (and paginates with NockerlPagination); each column's render(row)
// supplies the cell content.
import { useState } from 'react';
import { NockerlTable, type NockerlTableSort } from '@dizyx/nockerl-react';
export function SessionsTable({ rows, sort, selected, onSort, onSelect }: SessionsTableProps) {
return (
<NockerlTable
rows={rows} // rendered verbatim, so sort/paginate before passing in
getRowId={(r) => r.id}
columns={[
{ key: 'name', header: 'Session', sortable: true,
render: (r) => <NameCell status={r.status} label={r.name} /> },
{ key: 'status', header: 'Status', align: 'center', sortable: true, // chips/badges → center
render: (r) => <StatusPill status={r.status} /> },
// The alignment CANON: text left (default), numbers right (align: 'end'), chips center
// (align: 'center'); a mono column right-aligns by default. mono renders tabular figures.
{ key: 'tokens', header: 'Tokens', align: 'end', mono: true, sortable: true,
render: (r) => r.tokens.toLocaleString() },
{ key: 'cost', header: 'Cost', align: 'end', mono: true, sortable: true,
render: (r) => `$${r.cost.toFixed(2)}` },
{ key: 'updated', header: 'Updated', align: 'end', mono: true, sortable: true,
render: (r) => r.updatedLabel },
]}
sort={sort} // { key, dir: 'asc' | 'desc' } drives header arrow + aria-sort
onSortChange={onSort}
selectable // checkbox column + header select-all (indeterminate when partial)
selectedIds={selected}
onSelectionChange={onSelect}
density="comfortable" // 'comfortable' | 'compact' (padding only)
stickyHeader // header stays flush while the body scrolls
footer={(all) => <SummaryRow rows={all} />} // totals row pinned to the bottom
loading={false} // true → skeleton rows that hold the grid
empty={<EmptyState />} // shown when there are no rows
ariaLabel="Sessions"
/>
);
}

NockerlTable takes a rows array, a typed columns definition, and the cross-cutting concerns (sort, selection, density, sticky, footer, loading, empty) as props. Each NockerlTableColumn declares its own key / header, a render(row) cell, and per-column align / mono / sortable / width. v1 ships columns · sort · sticky · selection · empty · loading · density · footer; row virtualization and inline cell editing are a deliberate later slice. NockerlTable is controlled: it renders rows verbatim (sort + paginate before passing them in) and reports sort intent via onSortChange.

PropTypeDefaultDescription
columns *NockerlTableColumn<Row>[]The typed column definitions (order = column order).
rows *Row[]The rows to render, verbatim. The caller sorts + paginates before passing them in.
getRowId(row: Row, index: number) => stringStable id per row. It keys React and drives the selection set. Defaults to String(index).
sortNockerlTableSort | nullControlled sort state (the active column + direction), or null when unsorted.
onSortChange(next: NockerlTableSort) => voidFired with the next sort when a sortable header is clicked (toggles asc/desc on the same key).
selectablebooleanfalseAdd a checkbox column + a header select-all (indeterminate on a partial page).
selectedIdsSet<string>Controlled set of selected row ids (selectable).
onSelectionChange(next: Set<string>) => voidFired with the next selected-id set when a box (row or select-all) toggles.
stickyHeaderbooleanfalsePin the header (and footer) so they stay flush while the body scrolls.
loadingbooleanfalseRenders skeleton rows that hold the column grid; sets aria-busy.
loadingRowsnumber3How many skeleton rows to show while loading.
emptyReactNodeShown in place of the body when rows is empty (and not loading).
footer(rows: Row[]) => ReactNodeA summary row pinned under the body (column totals, for example). Receives the rendered rows.
densityNockerlTableDensity'comfortable'Row density. It adjusts vertical padding only, never the fill.
maxHeightstringMax height of the scroll region (proves the sticky header). A CSS length; omit for no cap.
ariaLabelstringAccessible name for the <table>.
captionstringA visually-hidden <caption> describing the table for assistive tech.