Dense Scan List
When to Use
- Browsing many records of one entity type (orders, POs, products, invoices) with GraphQL pagination
- Operators sort, filter, and select rows; row click navigates to detail
- Optionally: a bucket control (
Tabs, segmented buttons) aligned to one categorical dimension the business cares about (status, fulfillment stage, type)
Column Definition
Columns are defined with createColumnHelper (see the example below) — each column has a label and an accessor, or a custom render for cells like status badges.
Page Implementation
tsx
function DenseScan() {
const table = useDataTable({ data: { rows: orders, total: orders.length }, columns });
return (
<Layout>
<Layout.Header
title="Orders"
actions={[
<Button key="create" size="sm">
Create Order
</Button>,
]}
/>
<Layout.Column>
<DataTable.Root value={table}>
<DataTable.Toolbar>
<Input placeholder="Search orders..." className="max-w-sm" />
</DataTable.Toolbar>
<DataTable.Table />
<DataTable.Footer>
<DataTable.Pagination />
</DataTable.Footer>
</DataTable.Root>
</Layout.Column>
</Layout>
);
}Page Layout & Internal Scrolling
Table-first pages should pin their chrome and scroll only the rows region. Wrap the page in <Layout fill>:
fillstretches the layout to the available height and bounds the column row, so theDataTableshrinks to fit instead of growing past the viewport- The
Layout.Header(title/actions),DataTable.Toolbar, the column header row (sticky), andDataTable.Footer(pagination) stay visible at every viewport height — only the rows scroll vertically - When the current page of rows fits, nothing stretches and no scrollbar appears — short tables render identically with or without
fill - Requires no extra styling on the page: the height chain (
AppShellcontent area →Layout fill→Layout.Column→DataTable.Root) is wired by the components
Omit fill on pages that should flow and scroll naturally (forms, dashboards, articles) — the AppShell content area scrolls those.
Variants
- Toolbar chips only (
DataTable.Filters) — best when filters map cleanly to typed column metadata / enum facets - Tabs only above
DataTable— best when workflows are organized as obvious buckets - Tabs + chips — when buckets are primary and finer filters help
- Bulk selection —
onSelectionChangehook onuseDataTable; combine withinteraction/multi-select Tableprimitives — small static lists without collection hooks
Constraints
- Column count: 4-8 recommended
- Must include pagination — never render unbounded lists
- Table-first pages use
<Layout fill>so title/toolbar/header/footer stay pinned and only rows scroll - Handle every state:
DataTablerenders the loading skeleton and error row; always provide a labelled empty state (what the list is + how to add the first record) rather than a bare empty table - Status Badge colors must use design system tokens (variant prop): the primary status column uses filled semantic variants; secondary status columns (delivery, billing) use
outline-*(seedesign-system.md→ Composition & emphasis rules) - Bulk actions toolbar appears only when ≥1 row is selected
- Whole row is clickable via
onClickRow; wrap the primary identifier cell in<Link>for keyboard/SR access. No per-row "View" / "Open" buttons - Per-row
Menu(overflow…) is reserved for non-navigation actions (Archive, Duplicate, Delete) - Wide lists (many columns / horizontal scroll): pin the column users scan by to the left — the record's identifier (invoice / order reference) or its name (customer, product) — so it stays anchored while the rest scrolls; optionally pin a single high-signal status or total to the right. Keep the pinned set small (≈1 left, at most 1 right) — over-pinning eats the scroll area. Let users override via
<DataTable.Toolbar columnSettings>(show/hide + reorder + re-pin) with a stabletableIdso each user's layout persists
Anti-patterns
- Building a bespoke table + custom pagination instead of
DataTable+useCollectionVariables - Hand-rolled
max-height/overflowwrappers aroundDataTableto contain scrolling — use<Layout fill>instead - Tabs that mutate only local UI state while pagination/filters assume the full server set
- Using
<table>directly instead of<DataTable>for live collections - Client-side filtering on 1000+ records without server-side support
- Inline editable cells — use
pattern/detail/*orpattern/form/modalinstead - Per-row "View" / "Open" buttons duplicating the row-click navigation