← SpecThe anatomy of a screen specA field-by-field walk through screen_spec.json — every top-level section, what it declares, and how it cross-references the others.~10 min read
Top-level shapeEvery screen_spec.json is the same 11-key shape. Four are required (`type`, `version`, `metadata`, `structure`); the rest are optional but each carries its own scope — declare only what the screen actually uses. The key split below is the mental model the rest of this article walks down into: everything under `metadata` describes *what this screen is*, `stateManagement` and `dataFlow` describe *what it does*, and `structure` describes *what it looks like* (though when a `metadata.layoutFile` is set, `structure` is mostly populated by `layout_importer` at generate time).
screen_spec skeleton
// The skeleton every screen_spec.json starts from. Each top-level key// is either required (type / version / metadata / structure) or optional.{ "type": "screen_spec", "version": "1.0", "metadata": { /* name, displayName, platforms, layoutFile, … */ }, "structure": { /* components, layout, collection, tabView, … */ }, "stateManagement": { /* states, uiVariables, eventHandlers, displayLogic */ }, "dataFlow": { /* diagram, viewModel, customTypes, repositories, … */ }, "userActions": [ /* what the user taps and what the VM does about it */ ], "validation": { /* clientSide / serverSide rules */ }, "transitions": [ /* onNavigate(url) → destinations */ ], "relatedFiles": [ /* Layout / ViewModel / View / Model paths */ ], "notes": [ /* anything a reviewer needs to know next */ ]}metadataThe screen's identity block. `name` is PascalCase and becomes the generated class name across platforms — changing it is a rename refactor, not a cosmetic edit. `displayName` is the human-readable title and `description` is the 1-paragraph summary rendered on the overview of the generated HTML docs. `platforms` drives which outputs `jui build` distributes to. `layoutFile` points at the Layout JSON that owns the hierarchy (see `/spec/split-overview` Pattern 1). `parentSpec` turns this into a `screen_sub_spec` of another screen (Pattern 2). `createdAt` / `updatedAt` are used by the site index to order and age articles.
metadata
{ "metadata": { "name": "LearnHelloWorld", // PascalCase; becomes the class name "displayName": "Hello World", // human-readable title "description": "The five-minute first-screen tutorial.", "platforms": ["web"], // drives jui build's distribution "layoutFile": "learn/hello-world", // → docs/screens/layouts/learn/hello-world.json "parentSpec": "../home.spec.json", // optional — for screen_sub_spec "createdAt": "2026-04-22", "updatedAt": "2026-04-24" }}stateManagementFour slots for four different lifecycles. `states` is for explicit persistent state machines and is rarely used. `uiVariables` is the main event — every field the Layout JSON binds through `@{name}` must be declared here with a type and initial expression. `eventHandlers` lists every VM method the Layout binds an `onClick` to. `displayLogic` encodes conditional visibility / enabled rules without putting logic in the Layout — each entry maps a condition on state to effects on an element id, and the generator emits a derived `*Visibility` string the Layout binds to.
stateManagement
{ "stateManagement": { "states": [], // rarely used; persistent state machines "uiVariables": [ // everything the Layout binds to { "name": "count", "type": "Int", "initial": "0" }, { "name": "statusVisibility", "type": "String", "initial": "\"gone\"" } ], "eventHandlers": [ // every handler the Layout binds onClick to { "name": "onIncrement", "description": "Bump count; flip statusVisibility." } ], "displayLogic": [ // derive visibility / enabled from state { "condition": "count > 0", "effects": [{ "element": "status_label", "state": "visible", "variableName": "statusVisibility" }] } ] }}dataFlowThe architecture-level slot. `diagram` is a one-line Mermaid flowchart summarising how data moves — rendered inline in the generated HTML. `viewModel.methods` + `viewModel.vars` declare the public contract the hand-written VM must match (if those drift, `jui verify --fail-on-diff` fails). `customTypes` is where reusable row / entry shapes live — see `/spec/split-overview` Pattern 4 for how they cross-reference multiple specs. `repositories` / `useCases` / `apiEndpoints` carry architecture-level slots for data access (often empty for content screens, populated for feature screens).
dataFlow
{ "dataFlow": { "diagram": "flowchart TD\n VIEW --> VM\n VM -- rows --> VIEW", "viewModel": { // the public VM contract "methods": [ { "name": "onAppear", "description": "Seed rows." }, { "name": "onSelect", "params": [{ "name": "id", "type": "String" }] } ], "vars": [ { "name": "rows", "type": "Array(ActivityRow)", "observable": true } ] }, "customTypes": [ // reusable shapes (see Pattern 4) { "name": "ActivityRow", "properties": [ { "name": "id", "type": "String" }, { "name": "title", "type": "String" } ] } ], "repositories": [], "useCases": [], "apiEndpoints": [] // architecture-level slots }}structureThe visual tree. Eight sub-keys, but in practice most screens leave `components` / `layout` empty and let `layout_importer` inline the tree from `metadata.layoutFile` at generate time. `collection` and `tabView` are single-purpose slots only set when the screen is a root Collection page or a root TabView; `collections[]` (plural) is the first-class form for screens with several Collections — each entry is validated individually and gets its cell layouts generated, while tree placement stays with `metadata.layoutFile`. `decorativeElements` and `wrapperViews` annotate purely presentational children and structural wrappers — useful for reviewer communication, not required by the generator. `customComponents` is where you declare this screen uses an externally-specified custom component (see `/spec/split-overview` Pattern 3). `embeds[]` lists sub-screens this layout hosts as regions — each child owns its own ViewModel (see the screen composition concept article).`embeds[]` entries are: `{ regionId, screen, params?, events?, navigationMode? }`. The Layout JSON places each embed via `{ "type": "Embed", "id": <regionId>, ... }`. The embedded screen is unchanged — only the embedding (parent) spec declares the relationship. See /concepts/screen-composition and /reference/components/embed.
structure
{ // Most screens leave `structure` mostly empty and let layout_importer // inline the tree from metadata.layoutFile at generate time. "structure": { "components": [], // auto-imported from the Layout JSON "layout": {}, // same — flat tree of the Layout hierarchy "collection": null, // only set when the screen is a single-Collection page "tabView": null, // only set when the screen is a root TabView "decorativeElements": [], // purely presentational children "wrapperViews": [], // structural wrappers with a purpose note "customComponents": [ // see Pattern 3 (component_spec) { "name": "CodeBlock", "ref": "docs/components/json/codeblock.component.json" } ], "embeds": [ // Screen composition — child owns its own VM { "regionId": "detailPane", "screen": "order_detail", "params": { "orderId": "@{selectedOrderId}" }, "events": { "onOrderUpdated": "handleOrderUpdated" }, "navigationMode": "delegate" } ] }}Keep goingThe writer-facing tour above lays the ground. The two articles below take you into splitting decisions and the task-focused end-to-end flow.
Six ways to split a specMap article — layoutFile, parent + sub, component_spec, customTypes, cellClasses. Compare, pick the right one for your screen./spec/split-overview
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