UiRenderer
Renders a window from a JSON spec, for example one an assistant produced: a tree of library components with state, bindings, conditions and actions. Nothing has to exist in your code beforehand, and only allowlisted components and props can be used.
import { UiRenderer } from 'jarvis-react-ui/generative';
Basic
A spec has an id, initial state and a root node. Each node names a component (type) and
sets its props; { "$bind": "volume" } binds a control to a state value in both directions, and
visibleIf shows a node only while a condition holds. Text may contain {{key}} templates.
import { UiRenderer } from 'jarvis-react-ui/generative';
import type { UiWindowSpec } from 'jarvis-react-ui/generative';
// The kind of JSON an assistant returns when asked for "a volume control".
const SPEC: UiWindowSpec = {
id: 'volume',
state: { volume: 40, muted: false },
root: {
type: 'Stack',
props: { gap: 'md' },
children: [
{
type: 'Slider',
props: {
label: 'Volume',
value: { $bind: 'volume' },
showValue: true,
formatValue: '{value} %',
},
},
{ type: 'Switch', props: { label: 'Mute', checked: { $bind: 'muted' } } },
{
type: 'Pill',
props: { variant: 'warn' },
children: 'Muted',
visibleIf: { state: 'muted' },
},
{
type: 'Mono',
children: 'Output at {{volume}} %',
visibleIf: { state: 'muted', eq: false },
},
],
},
};
export default function Basic() {
return <UiRenderer spec={SPEC} />;
}
The spec
{
"id": "volume",
"title": "Volume",
"size": { "w": 360, "h": 240 },
"state": { "volume": 40, "muted": false },
"root": {
"type": "Stack",
"children": [
{
"type": "Slider",
"props": {
"label": "Volume",
"value": { "$bind": "volume" },
"formatValue": "{value} %"
}
},
{
"type": "Mono",
"children": "Output at {{volume}} %",
"visibleIf": { "state": "muted", "eq": false }
},
{
"type": "Button",
"children": "Apply",
"on": { "onClick": { "emit": "apply", "payload": { "$state": "volume" } } }
}
]
}
}
| Part | Meaning |
|---|---|
type |
A component from the allowlist (UI_REGISTRY); others render a placeholder. |
props |
Allowed props of that component; others are ignored. |
{ "$state": "k" } |
Reads a state value. |
{ "$bind": "k" } |
Two-way binding on value / checked; the change handler is wired for you. |
"…{{k}}…" |
Template in any text. |
children |
Text, or an array of nodes for layout components. |
on |
Actions per event prop: { "set": "k", "value"? }, { "toggle": "k" }, { "emit": "name", "payload"? }. |
visibleIf |
{ "state": "k" } (truthy), { "state": "k", "eq": v } or { "state": "k", "ne": v }. |
id |
Makes a player addressable through call(...). |
Some props are translated: text props (label, title, …) take text or a nested node, icon props
take a name from UI_ICONS ("Play"), format functions take a template ("{value} %"), Table
takes columns: [{ key, header, align? }] and rows objects, and Tabs items take text or a node
as content.
Several windows
useUiWindows() manages generated windows for a WindowManager: open
validates a spec and opens it as a floating window (an existing id is replaced), patchState
changes a window's state without rebuilding it, call controls players, and onAction receives
every emit. Try opening both windows, then the buttons.
import { useState } from 'react';
import { Button, Mono, Stack, WindowManager } from 'jarvis-react-ui';
import { useUiWindows } from 'jarvis-react-ui/generative';
import type { UiWindowSpec } from 'jarvis-react-ui/generative';
const VIDEO: UiWindowSpec = {
id: 'video',
title: 'Video',
size: { w: 420, h: 320 },
root: { type: 'YouTubePlayer', id: 'player', props: { videoId: 'aqz-KE-bpKQ', size: 'sm' } },
};
const VOLUME: UiWindowSpec = {
id: 'volume',
title: 'Volume',
size: { w: 280, h: 160 },
state: { volume: 40 },
root: {
type: 'Stack',
children: [
{
type: 'Slider',
props: { label: 'Volume', value: { $bind: 'volume' }, showValue: true },
},
{
type: 'Button',
children: 'Apply',
on: { onClick: { emit: 'apply_volume', payload: { $state: 'volume' } } },
},
],
},
};
export default function Windows() {
const [log, setLog] = useState('—');
const ui = useUiWindows({
onAction: (e) => setLog(`${e.windowId} → ${e.name}(${JSON.stringify(e.payload)})`),
});
return (
<>
<Stack direction="row" gap="sm" wrap className="absolute top-3 left-3 z-[1]">
<Button size="sm" onClick={() => ui.open(VIDEO)}>
Open video
</Button>
<Button size="sm" onClick={() => ui.open(VOLUME)}>
Open volume
</Button>
<Button size="sm" onClick={() => ui.patchState('volume', { volume: 20 })}>
Set volume 20
</Button>
<Button size="sm" onClick={() => ui.call('video', 'player', 'seek', 90)}>
Jump to 1:30
</Button>
<Mono size="xs" secondary>
{log}
</Mono>
</Stack>
<WindowManager
windows={ui.windows}
assignments={{}}
onAssignmentsChange={() => undefined}
onClose={ui.onClose}
/>
</>
);
}
Validation
validateUiSpec(spec) returns every problem with its JSON path, so you can send them back to the
model and ask for a fixed spec. open refuses invalid specs; UiRenderer renders what it can and
reports the rest through onError.
import { CodeBlock } from 'jarvis-react-ui';
import { validateUiSpec } from 'jarvis-react-ui/generative';
// A spec with mistakes, as a model might produce it.
const SPEC = {
id: 'broken',
state: { level: 3 },
root: {
type: 'Stack',
children: [
{ type: 'ThreeOrb' },
{ type: 'Slider', props: { value: { $bind: 'volume' }, color: 'red' } },
{ type: 'Link', props: { href: 'javascript:alert(1)' }, children: 'Click' },
],
},
};
export default function Validation() {
const { errors } = validateUiSpec(SPEC);
const text = errors.map((e) => `${e.path}: ${e.message}`).join('\n');
return <CodeBlock code={text} language="text" title="validateUiSpec(spec).errors" />;
}
Safety
- Only components and props in
UI_REGISTRYare rendered. There is no way to setclassName, inline styles, raw HTML (CodeBlock.htmlis not allowed) or event handlers other than actions. - Links, media sources and web frames accept only
http(s)URLs or relative paths;WebFrame's sandbox cannot be changed from a spec. - Text is always rendered as text. Specs are limited to 24 levels and 500 nodes.
Accessibility
The rendered components keep their own semantics and keyboard support. Ask the model for labels:
label on form controls, aria-label where there is no visible text.
API
| Prop | Type | Default | Description |
|---|---|---|---|
spec (required) |
UiWindowSpec |
The window spec to render; only its root and initial state are used here. |
|
onAction |
((event: UiActionEvent) => void) |
Called for every emit action. |
|
onError |
((errors: UiSpecError[]) => void) |
Called with the problems validateUiSpec finds; invalid parts render as placeholders. |
|
onStateChange |
((state: UiState) => void) |
Called with the full next state whenever a binding or a set / toggle action changes it. |
|
state |
UiState |
Controlled state. Leave undefined to keep the state inside the renderer, starting from spec.state. |
Also accepts inherited props from RefAttributes (1), Attributes (1).