---
url: https://docs.tailor.tech/app-shell/patterns/list-dense-scan.md
description: >-
  High-density scannable list backed by GraphQL connections with DataTable,
  sort, filters, and pagination
---

# 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>`:

* `fill` stretches the layout to the available height and bounds the column row, so the `DataTable` shrinks to fit instead of growing past the viewport
* The `Layout.Header` (title/actions), `DataTable.Toolbar`, the column header row (sticky), and `DataTable.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 (`AppShell` content 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** — `onSelectionChange` hook on `useDataTable`; combine with `interaction/multi-select`
* **`Table` primitives** — 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: `DataTable` renders 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-*`** (see `design-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 stable `tableId` so each user's layout persists

## Anti-patterns

* Building a bespoke table + custom pagination instead of `DataTable` + `useCollectionVariables`
* Hand-rolled `max-height`/`overflow` wrappers around `DataTable` to 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/*` or `pattern/form/modal` instead
* Per-row "View" / "Open" buttons duplicating the row-click navigation
