You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: DataTable internal scrolling with pinned chrome via Layout fill
Implements internal scrolling for DataTable inside the AppShell layout
(tailor-inc/platform-planning#1388):
- Shell is now viewport-bounded (h-svh) with the content area taking over
scroll duty (overflow-y-auto, full-bleed so the scrollbar sits at the
window edge), so scrolling never happens on the document
- New opt-in `fill` prop on Layout stretches the page to the available
height and bounds the column row (grid minmax(0,1fr)) so children can
scroll internally
- DataTable.Root becomes a shrinkable flex column; the table container is
the vertical scroll region with a sticky column header row; Toolbar and
Footer stay pinned
- Empty/error state height fixed at 3 rows (was pageSize-worth, creating a
huge blank/scrollable region at large page sizes); message is sticky so
it stays in view in height-constrained containers
- Skeleton loader rows derive their height from the same structure as real
rows (icon-button footprint, text line box, badge pills) so the table
no longer shifts when data resolves
Examples: products page uses <Layout fill> with ~200 mock rows and a
25-row default page size; new long-content page verifies content-area
scrolling for pages without fill. Pattern docs (list/dense-scan) and
Layout docs updated and regenerated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DataTable now scrolls internally: add `fill` prop to `Layout` to pin page chrome and scroll only the table rows.
6
+
7
+
```tsx
8
+
<Layoutfill>
9
+
<Layout.Headertitle="Products" />
10
+
<Layout.Column>
11
+
<DataTable.Rootvalue={table}>
12
+
<DataTable.Toolbar>…</DataTable.Toolbar>
13
+
<DataTable.Table />
14
+
<DataTable.Footer>
15
+
<DataTable.Pagination />
16
+
</DataTable.Footer>
17
+
</DataTable.Root>
18
+
</Layout.Column>
19
+
</Layout>
20
+
```
21
+
22
+
With `fill`, the page title, table toolbar, column header row (sticky), and footer stay visible at all heights — only the rows region scrolls vertically. Without `fill`, pages grow and scroll as before. Tables short enough to fit render without a scrollbar or layout shift.
23
+
24
+
**Behavior change:** the AppShell layout is now viewport-bounded (`h-svh` instead of `min-h-svh`), so page content scrolls inside the content area rather than on the document. Code relying on `window`/document scroll position should target the content area instead.
25
+
26
+
Also fixes the empty/error state reserving `pageSize`-worth of height — it is now capped at 5 rows, so large page sizes no longer produce a huge blank region below "No data".
**`fill`** — stretches the layout to the available height and bounds the column row so children can scroll internally instead of growing the page. Use on table-first pages (`<Layout fill>` + `DataTable`): the title, table toolbar, sticky column headers, and pagination footer stay pinned while only the rows scroll — see **`patterns/list-dense-scan.md`**. Omit on pages that should flow and scroll naturally (forms, dashboards).
51
53
52
54
**Column-count → width rules** (column count is auto-detected from `Layout.Column` children):
Copy file name to clipboardExpand all lines: catalogue/src/pattern/list/dense-scan/PATTERN.md
+13Lines changed: 13 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -45,6 +45,17 @@ dont:
45
45
46
46
<!-- source: dense-scan.tsx -->
47
47
48
+
## Page Layout & Internal Scrolling
49
+
50
+
Table-first pages should pin their chrome and scroll only the rows region. Wrap the page in `<Layout fill>`:
51
+
52
+
-`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
53
+
- 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
54
+
- When the current page of rows fits, nothing stretches and no scrollbar appears — short tables render identically with or without `fill`
55
+
- Requires no extra styling on the page: the height chain (`AppShell` content area → `Layout fill` → `Layout.Column` → `DataTable.Root`) is wired by the components
56
+
57
+
Omit `fill` on pages that should flow and scroll naturally (forms, dashboards, articles) — the AppShell content area scrolls those.
58
+
48
59
## Variants
49
60
50
61
-**Toolbar chips only (`DataTable.Filters`)** — best when filters map cleanly to typed column metadata / enum facets
@@ -57,6 +68,7 @@ dont:
57
68
58
69
- Column count: 4-8 recommended
59
70
- Must include pagination — never render unbounded lists
71
+
- Table-first pages use `<Layout fill>` so title/toolbar/header/footer stay pinned and only rows scroll
60
72
- Status Badge colors must use design system tokens (variant prop)
61
73
- Bulk actions toolbar appears only when ≥1 row is selected
62
74
- Whole row is clickable via `onClickRow`; no per-row "View" / "Open" buttons
@@ -65,6 +77,7 @@ dont:
65
77
## Anti-patterns
66
78
67
79
- Building a bespoke table + custom pagination instead of `DataTable` + `useCollectionVariables`
80
+
- Hand-rolled `max-height`/`overflow` wrappers around `DataTable` to contain scrolling — use `<Layout fill>` instead
68
81
- Tabs that mutate only local UI state while pagination/filters assume the full server set
69
82
- Using `<table>` directly instead of `<DataTable>` for live collections
70
83
- Client-side filtering on 1000+ records without server-side support
|`fill`|`boolean`|`false`| Fill the available height instead of growing with content (see [Fill Mode](#fill-mode)) |
43
+
|`columns`|`1 \| 2 \| 3`| - |**(Deprecated)** Auto-detected from `Layout.Column` children count when omitted |
44
+
|`title`|`string`| - |**(Deprecated)** Use `<Layout.Header title="...">` instead |
45
+
|`actions`|`React.ReactNode[]`| - |**(Deprecated)** Use `<Layout.Header actions={[...]}>` instead |
46
+
|`gap`|`number`|`4`|**(Deprecated)** Use `className` (e.g. `className="gap-6"`) instead |
46
47
47
48
### Layout.Header Props
48
49
@@ -228,6 +229,31 @@ If any `Layout.Column` has an `area` prop, all columns switch to area-based widt
228
229
229
230
Columns are rendered in source order — place them in the visual order you want.
230
231
232
+
## Fill Mode
233
+
234
+
By default, `<Layout>` grows with its content and the AppShell content area scrolls. Set `fill` to instead stretch the layout to the available height and bound its column row, so a child component can scroll **internally** while the page chrome stays pinned:
235
+
236
+
```tsx
237
+
<Layoutfill>
238
+
<Layout.Headertitle="Products" />
239
+
<Layout.Column>
240
+
<DataTable.Rootvalue={table}>
241
+
<DataTable.Toolbar>
242
+
<DataTable.Filters />
243
+
</DataTable.Toolbar>
244
+
<DataTable.Table />
245
+
<DataTable.Footer>
246
+
<DataTable.Pagination />
247
+
</DataTable.Footer>
248
+
</DataTable.Root>
249
+
</Layout.Column>
250
+
</Layout>
251
+
```
252
+
253
+
With `fill`, the `Layout.Header` (title/actions), the `DataTable` toolbar, its column header row, and its footer all remain visible regardless of row count — only the table's rows region scrolls vertically. When the content is short enough to fit, nothing stretches and no scrollbar appears.
254
+
255
+
Use `fill` for pages whose main content manages its own scrolling (typically a `DataTable`). Leave it off for pages that should flow and scroll naturally (forms, articles, dashboards). It is intended for single-row layouts; when multiple columns stack vertically on mobile, the content area scrolls as usual.
256
+
231
257
## Gap Spacing
232
258
233
259
Use `className` to control the space between columns:
(name)=>`Compact, reliable ${name.toLowerCase()} with a clean finish and a hassle-free setup.`,
406
+
(name,category)=>
407
+
`${name} designed for ${category.toLowerCase()} setups, balancing durability and comfort for everyday professional use.`,
408
+
(name,category)=>
409
+
`Premium ${category.toLowerCase()}-grade ${name.toLowerCase()} featuring reinforced construction, thoughtful cable routing, and a warranty-backed build engineered to hold up across years of daily use.`,
410
+
];
411
+
412
+
constpad3=(n: number)=>String(n).padStart(3,"0");
413
+
constpad2=(n: number)=>String(n).padStart(2,"0");
414
+
415
+
// Fixed base so all generated dates are deterministic (2026-01-01T00:00:00Z).
0 commit comments