← Split overviewCollection cell classesPattern 5 of spec splitting, in detail. When a Collection needs more than one cell layout, or when the same cell shape shows up in two or more Collections, hoist each cell into `docs/screens/layouts/cells/<name>.json` and reference it by path. `jui build` inlines the referenced cell at generate time and emits one typed component per cell.~7 min read
Why hoist cells outTwo triggers. (1) A Collection needs to interleave different cell shapes — a feed that mixes post cards with ad slots, a settings screen that mixes toggle rows with stepper rows. You can't express 'different cell per section' without a ref, so the cells have to live somewhere. (2) The same cell shape shows up in multiple Collections across the site — `next_step_card` is used at the bottom of almost every article here. Promoting it once lets every Collection reuse the same generated component and the same Data type.
The shapeA Collection's `sections[]` is an array — each entry declares `cell` (a path ref like `"cells/feed_card"`) plus the binding for `items`. The cell layout itself is a standalone Layout JSON with its own data block declaring the props the cell takes. At generate time the cell tree is inlined and a typed cell component is emitted, so the call site treats the cell like any other View.
Collection + cell layouts
// A Collection that renders two different cells — feed_card for posts,// ad_slot every Nth row — driven by sections[].cell.{ "type": "Collection", "id": "feed_collection", "items": "@{rows}", "cellIdProperty": "id", "sections": [ { "cell": "cells/feed_card", "items": "@{posts}" }, { "cell": "cells/ad_slot", "items": "@{ads}" } ]} // The two referenced cells live as their own Layout JSON files.// docs/screens/layouts/cells/feed_card.json:{ "type": "View", "orientation": "vertical", "child": [ { "data": [ { "name": "titleKey", "class": "String" }, { "name": "bodyKey", "class": "String" }, { "name": "onTap", "class": "() -> Void" } ]}, { "type": "Label", "text": "@{titleKey}", "fontSize": 18 }, { "type": "Label", "text": "@{bodyKey}", "fontSize": 14, "fontColor": "#475467" }, { "type": "Button", "text": "Open", "onClick": "@{onTap}" } ]}Reference forms + directory layoutThe cell ref is a path relative to the layouts directory (`docs/screens/layouts/` by default). The `.json` suffix is optional. Nested subdirectories work too. The convention on this site — which is not mandatory but is followed by every cell — is `docs/screens/layouts/cells/<kebab_name>.json`, one file per cell shape. 28 cells live there today; grep the source for `"cell": "cells/<name>"` to find reuse sites for any of them.
ref forms + on-disk location
Cell reference forms--------------------"cells/feed_card" path under docs/screens/layouts/ (preferred)"cells/feed_card.json" same — the .json suffix is optional"learn/hello-world/tab" nested subdirectory; also valid Where cells live----------------docs/screens/layouts/└── cells/ ← by convention, reusable cells sit here ├── next_step_card.json ← appears at the bottom of every article on this site ├── prereq_row.json ← used by /learn/installation and /learn/hello-world ├── quickstart_step.json ← used by /learn/hello-world ├── tab_header.json ← used everywhere a platform tab switcher shows up └── verify_row.json ← /learn/installation's 3-row verify table 28 cells live under docs/screens/layouts/cells/ today. Grep the site for`"cell": "cells/<name>"` to find reuse sites.What gets inlined at generate time`layout_importer._expand_collection_refs` walks the spec, finds every Collection, resolves each `sections[].cell` ref, reads the cell Layout JSON, and inlines the tree under `structure.collection._resolvedCells[<ref>]`. Bindings referenced in the cell become the cell's own `uiVariables`. The generator emits a typed cell component per referenced cell plus a per-cell Data type (on web: `src/generated/components/cells/NextStepCard.tsx` + `src/generated/data/NextStepCardData.ts`), and the screen's Collection renders those typed cells at the call site.
expand flow
# What layout_importer does for Collection cell references at generate time# (see document_tools/.../layout_importer.py:_expand_collection_refs): 1. Walk the enriched spec looking for Collection nodes2. For each `sections[].cell` entry, resolve the path under layouts_directory and read the cell Layout JSON3. Inline the cell tree under structure.collection._resolvedCells[<ref>] (keyed by the ref string so the same cell referenced twice is inlined once)4. Harvest every binding (@{...}) the cell references as its own uiVariables on the cell data block — these become the cell's prop shape5. The generator emits one <CellName>.tsx / .swift / .kt per cell, plus a <CellName>Data type for the inlined prop shape (web: src/generated/data/ NextStepCardData.ts for next_step_card) Result: the screen spec references cells by path; the runtime gets real,typed components. No duplication, and each cell is testable in isolation.structure.collection.cellClassesFor screens whose top-level is a Collection (not a regular View containing a Collection), the spec's `structure.collection` slot becomes the screen's backbone. `cellClasses` lists every cell ref the Collection might use, and `sections[]` picks one per section at render time. For most screens on this site the Collection is nested inside a regular View and the cell refs live directly in the sections without a top-level `cellClasses` declaration — either form is valid, and the top-level `cellClasses` field is an affordance for screen-is-a-Collection layouts.
Live examples on this siteThe most-reused cell on this site is `next_step_card` — the 2-column card that sits under every article's 'Keep going' heading, pointing readers at the next article. `prereq_row` appears in `/learn/installation` and `/learn/hello-world`, `quickstart_step` in `/learn/hello-world` (three platform tabs each with their own steps), `tab_header` everywhere a platform tab switcher shows up. Single-use cells are fine too — `figma_throttle_row` on `/tools/doc` isn't reused anywhere, but lives in `cells/` so it compiles the same way as every other cell.
When NOT to hoistTwo anti-patterns. (1) A one-off cell whose body is three Labels — the ceremony (separate file + separate Data type + typed component) outweighs the reuse benefit. Keep it inline inside the parent Layout JSON. (2) Trying to share behaviour (a tap handler, a state machine) by hoisting cells — cells share *shape*, not behaviour. Behaviour lives in the ViewModel; the cell takes closures as props and the parent VM supplies them. If what you want to share is the behaviour, reach for Pattern 3 (component_spec) or Pattern 4 (customTypes) instead.
Keep goingAll five patterns covered. Head back to the overview for the picker cheat sheet, or revisit any pattern you want to drill into further.
Custom typesPattern 4 — declare shared row / entry shapes once in dataFlow.customTypes./spec/custom-types
Six ways to split a specBack to the map article with the picker cheat sheet./spec/split-overview