← 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.`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.
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 }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 3same 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