JsonUI
← SpecSix ways to split a specA single screen_spec.json is fine until the file passes 300 lines, or until two screens need to share the same shape. This article is the map: six established split patterns, what each one buys you, and how to pick.~7 min read
Why split at allThree triggers push a single spec past its weight class: (1) the file grows past reviewability — anything over ~300 lines starts producing merge conflicts every review; (2) two or more screens need the same shape — a feed screen and an archive screen both rendering the same row model, for example; (3) a custom UI keeps reappearing across pages — a chart, a table-of-contents, a code sample. Each trigger points at a different pattern below, so diagnose first, split second.
Pattern 1 — layoutFile extractionMove the visual tree into docs/screens/layouts/<path>.json and point `metadata.layoutFile` at it. The spec keeps the contract (uiVariables, eventHandlers, customTypes); the Layout JSON owns the hierarchy. At generate time layout_importer pulls the tree back into `structure.components`, so the spec still validates standalone. This is the most common split — almost every screen on this site uses it.
recent-activity.spec.json
// screen_spec.json — declares the contract and points at the layout file.
{
"type": "screen_spec",
"metadata": {
"name": "RecentActivity",
"layoutFile": "recent-activity" // resolves to docs/screens/layouts/recent-activity.json
},
"structure": {
"components": [], // empty — layout_importer populates at generate time
"layout": {}
}
}
Pattern 2 — parent + sub specSame screen, multiple spec files. The parent sets `type: 'screen_parent_spec'` and lists its children in `subSpecs[]`. Each child sets `type: 'screen_sub_spec'` and names the parent in `metadata.parentSpec`, inheriting `structure` and `layoutFile` while declaring only its own slice of `stateManagement`. Use this when one screen has 20+ state variables that cleanly partition per region and you want per-region code review.
parent + one sub
// home.spec.json — the parent. Owns the Layout and the sub-spec roster.
{
"type": "screen_parent_spec",
"metadata": { "name": "Home", "layoutFile": "home" },
"subSpecs": [
{ "file": "home/feed.spec.json", "name": "HomeFeed" },
{ "file": "home/notifications.spec.json", "name": "HomeNotifications" }
]
}
 
// home/feed.spec.json — one sub. Declares only its slice of state.
{
"type": "screen_sub_spec",
"metadata": {
"name": "HomeFeed",
"description": "Feed region state + handlers.",
"parentSpec": "../home.spec.json" // layoutFile is inherited from the parent
},
"stateManagement": {
"uiVariables": [ { "name": "feedRows", "type": "[FeedRow]", "initial": "[]" } ],
"eventHandlers": [ { "name": "onRefreshFeed" } ]
}
}
Pattern 3 — component_specReusable custom UI (CodeBlock, TableOfContents, Chart, …) lives in `docs/components/json/<name>.component.json` with `type: 'component_spec'`. Each spec declares `props`, `slots`, and a `platformMapping` block. Any screen can import it through `structure.customComponents`; the generator emits a platform-native component per platform, while the .component.json stays the single contract all of them compile against. This site ships five of these today — CodeBlock, Sidebar, TableOfContents, TopBar, DocSamplePreview.
component spec + screen reference
// docs/components/json/chart.component.json — the reusable contract.
{
"type": "component_spec",
"metadata": { "name": "Chart", "category": "display" },
"props": {
"data": { "type": "[ChartPoint]", "required": true },
"height": { "type": "Int", "default": 240 }
}
}
 
// Any screen spec can now reference it:
{
"structure": {
"customComponents": [
{ "name": "Chart", "ref": "docs/components/json/chart.component.json" }
]
}
}
Pattern 4 — customTypesDeclare a shared row or entry shape once inside `dataFlow.customTypes`, then reference it by name (`[ActivityRow]`) from any `uiVariables` or VM var. Stays within one file by default; register the name in `.jsonui-type-map.json` and two specs can share the same type. Lighter-weight than splitting into a sub-spec — use when the thing you want to DRY up is the *type shape*, not a slice of state.
dataFlow.customTypes
// Declare the shape once in dataFlow.customTypes...
{
"dataFlow": {
"customTypes": [
{
"name": "ActivityRow",
"properties": [
{ "name": "id", "type": "String" },
{ "name": "title", "type": "String" },
{ "name": "url", "type": "String" }
]
}
]
},
"stateManagement": {
"uiVariables": [
{ "name": "rows", "type": "[ActivityRow]", "initial": "[]" }
]
}
}
 
// ...then reference it by name from any spec. Register it in
// .jsonui-type-map.json to cross-reference across files.
Pattern 5 — cellClassesWhen a Collection needs more than one cell layout — a feed interleaving post cards with ad slots, for example — hoist each cell into `docs/screens/layouts/cells/<name>.json` and list them in `structure.collection.cellClasses[]`. `sections[].cell` picks which cell a section renders. `jui build` inlines each referenced cell at generate time, and the same cell file can be re-used by any other Collection across the site.
multi-cell Collection
// A Collection with two cell layouts — each cell lives in its own file.
{
"structure": {
"collection": {
"cellClasses": [
"cells/feed_card", // docs/screens/layouts/cells/feed_card.json
"cells/ad_slot" // docs/screens/layouts/cells/ad_slot.json
],
"sections": [
{ "cell": "cells/feed_card", "items": "@{posts}" },
{ "cell": "cells/ad_slot", "items": "@{ads}" }
]
}
}
}
 
// `jui build` inlines each referenced cell layout into the generated
// component, so the same cell can be reused by any other Collection.
Pattern 6 — Embed (sub-screens with their own ViewModel)When a region of a screen needs its own independent ViewModel — a tablet master/detail where the detail pane is conceptually a separate screen, or a dashboard panel with its own data flow — declare each region in `structure.embeds[]` and place an `{ "type": "Embed", ... }` element in the parent Layout. The embedded screen is unchanged: only the embedding (parent) spec is aware of the relationship. `params` (parent → child) and `events` (child → parent) are the only cross-screen channels. Child VMs that implement `applyInitParams(_:)` consume params; others ignore them.
structure.embeds[] + Embed in Layout
// Parent spec hosts an `OrderDetail` screen as a region. The embedded
// screen is unchanged — only the parent declares the relationship.
{
"metadata": { "name": "OrdersDashboard", "layoutFile": "orders/dashboard" },
"structure": {
"embeds": [
{
"regionId": "detailPane",
"screen": "order_detail",
"params": { "orderId": "@{selectedOrderId}" },
"events": { "onOrderUpdated": "handleOrderUpdated" },
"navigationMode": "delegate"
}
]
}
}
 
// Parent Layout JSON places the embed in the tree:
{ "type": "Embed", "id": "detailPane", "screen": "order_detail",
"params": { "orderId": "@{selectedOrderId}" }, "weight": 1 }
`navigationMode` picks how the child navigates: `"delegate"` (default) shares the parent's NavController/Router — `push` bubbles to the parent while `pop` / `dismiss` / `navigateBack` are bounded at the embed; `"isolated"` gives the embed a private nested stack — push stays inside, pop stops at the embed's root. See /concepts/screen-composition for the trade-offs against `include` and `TabView`, and /reference/components/embed for the full attribute reference.
Picking the right patternThe triggers from 'Why split at all' map onto the six patterns in predictable ways — use the cheat sheet below as a starting point, then follow the relevant detail article once it lands in Phase 2 of the spec-authoring rewrite.
decision flow
file grew past ~300 lines? ...................... Pattern 1 (layoutFile)
two screens share the SAME shape? ............... Pattern 1 or Pattern 3
same visual tree, different state per region? .... Pattern 2 (parent + sub)
reusable UI across the whole site? ............... Pattern 3 (component_spec)
same row model across multiple screens? ......... Pattern 4 (customTypes)
Collection needs more than one cell? ............. Pattern 5 (cellClasses)
region needs its own ViewModel (master/detail)? . Pattern 6 (Embed)
Keep goingPick the split that fits, then dive into the field-level anatomy or the end-to-end writing flow.
The anatomy of a screen specField-by-field dictionary of screen_spec.json — every top-level section and how it cross-references the others./spec/anatomy
Writing your first specTask-focused 10-step guide: author a screen_spec.json end-to-end for a counter screen./guides/writing-your-first-spec