JsonUI
← Split overviewComponent specsPattern 3 of spec splitting, in detail. When a custom UI shows up on two or more screens, extract its contract into `docs/components/json/<name>.component.json`. Screens import it through `structure.customComponents`; the generator emits a platform-native component per target. This site ships five of these today — CodeBlock, Sidebar, TableOfContents, TopBar, and DocSamplePreview — and the same pattern scales to dozens.~10 min read
When to extractThe trigger is repetition across screens, not size. A 50-line inline View that only appears on one screen stays inline. A 30-line chart shape that shows up on three dashboards belongs in a component_spec. The break-even rule of thumb: two reuses pay for the spec + converter pair; three reuses make it obviously worth it. If it's still a one-off, stay with inline.
The spec shape`type: 'component_spec'`, a `metadata` block (name, category, description), a `props` map that declares the public API (type + required / default / enum / description per prop), an optional `slots` map for named children, and a `platformMapping` block that tells each platform's converter where to emit the implementation. Event props (functions) live in `props` too — their `type` carries the signature.
component_spec shape
// docs/components/json/chart.component.json
{
"type": "component_spec",
"version": "1.0",
"metadata": {
"name": "Chart",
"category": "display", // one of: display, input, container, overlay
"description": "Line / bar chart for time-series metrics."
},
"props": {
"data": { "type": "[ChartPoint]", "required": true,
"description": "Points sorted by x." },
"kind": { "type": "String", "default": "line",
"enum": ["line", "bar", "area"] },
"height": { "type": "Int", "default": 240 },
"onPointTap": { "type": "(ChartPoint) -> Void" } // event prop
},
"slots": { // optional — named children
"legend": { "type": "View?", "description": "Rendered above the chart." }
},
"platformMapping": { // generator hints
"web": { "emit": "extensions/Chart.tsx", "tagName": "Chart" },
"ios": { "emit": "Views/Chart.swift", "tagName": "Chart" },
"android": { "emit": "ui/Chart.kt", "tagName": "Chart" }
}
}
Where things live on diskThe spec goes in `docs/components/json/<name>.component.json` — authored, committed, the single source of truth. The emitted implementation lives in each platform root's `extensions/` directory (`jsonui-doc-web/src/components/extensions/Chart.tsx` on web, for example). After the one-time scaffold the implementation file is hand-edited — the converter is what turns the Layout JSON reference into a call site against it.
on-disk placement
docs/
└── components/
└── json/ ← authored component specs live here
├── chart.component.json
├── codeblock.component.json ← ships today
├── doc-sample-preview.component.json
├── search.component.json
├── sidebar.component.json
├── tableofcontents.component.json
└── topbar.component.json
 
jsonui-doc-web/
└── src/
└── components/
└── extensions/ ← hand-authored component implementations
├── CodeBlock.tsx ← emitted once, edited by hand thereafter
├── DocSamplePreview.tsx
└── Chart.tsx ← (would live here after `jui g converter`)
Referencing from a screenTwo steps. (1) Add the component to `structure.customComponents` in the screen spec with `{ name, ref }` — the `ref` is the path to the component_spec.json. (2) Use it in the Layout JSON as if it were a built-in View type: the prop names come straight from the component_spec's `props` map. Bind values with `@{…}`, pass static props inline, and wire events with `@{handler}` just like a built-in.
screen spec + Layout usage
// Any screen spec imports a component by adding it to
// structure.customComponents and then using it in the Layout JSON.
 
// analytics.spec.json — references the Chart component.
{
"metadata": { "name": "Analytics", "layoutFile": "analytics" },
"structure": {
"customComponents": [
{
"name": "Chart",
"ref": "docs/components/json/chart.component.json"
}
]
}
}
 
// docs/screens/layouts/analytics.json — uses the component like any other View type.
{
"type": "View", "orientation": "vertical",
"child": [
{ "type": "Chart",
"width": "matchParent",
"height": 260,
"data": "@{metricsPoints}", // prop binding
"kind": "area", // static prop
"onPointTap": "@{onSelectPoint}" // event binding
}
]
}
Scaffolding the implementation`rjui g converter <Name>` (web) / `sjui g converter <Name>` (iOS) / `kjui g converter <Name>` (Android) creates two files per platform: a converter `.rb` file under `rjui_tools/lib/react/converters/extensions/` (or the Swift / Kotlin equivalent) that translates the Layout JSON reference into a platform call site, and a stub implementation file under the platform's `extensions/` directory. The stub is the starting point — you edit it by hand; the converter is the machine-readable glue.
one-time scaffold
# One-time: scaffold the platform-native component skeleton from the spec.
# Generates one converter + one component impl per target platform.
 
# Web — emits jsonui-doc-web/src/components/extensions/Chart.tsx
# and jsonui-doc-web/rjui_tools/lib/react/converters/extensions/chart_converter.rb
rjui g converter Chart
 
# iOS — equivalent for Swift
sjui g converter Chart
 
# Android
kjui g converter Chart
 
# After scaffolding you edit the component body by hand (the converter is
# the machine-readable translator from Layout JSON → platform source).
# The component_spec.json remains the single source of truth for props,
# slots, and events that the converter compiles against.
Live examples on this siteFive component specs ship today: `codeblock.component.json` (the syntax-highlighted code snippet you're reading this next to), `doc-sample-preview.component.json` (the iframe wrapper on `/tools/doc`), `search.component.json`, `sidebar.component.json`, `tableofcontents.component.json`, and `topbar.component.json`. `codeblock` is the richest — copy button, line numbers, highlight lines, theme — and a good read if you want a real, non-trivial example to start from.
When NOT to extractTwo anti-patterns. (1) Extracting a one-off View just because it's long — readers have to jump to another file for something they'll only ever see once. (2) Extracting a composition of built-ins that doesn't need platform-native code — if you can express it in Layout JSON alone, that's Pattern 1 (a shared Layout JSON) or Pattern 5 (a reusable cell), not a component_spec. component_spec is for things that need *code*, not *structure*, to be reusable.
Keep goingThree patterns down, two to go — customTypes and cellClasses.
Parent + sub specsPattern 2 — split one screen into multiple spec files by region of state./spec/parent-sub-spec
Custom typesPattern 4 — declare shared row / entry shapes once in dataFlow.customTypes, reference by name across specs./spec/custom-types