JsonUI
← 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 nodes
2. For each `sections[].cell` entry, resolve the path under layouts_directory
and read the cell Layout JSON
3. 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 shape
5. 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