Skip to content

Wire Format Specification ​

The $prefab wire format is the JSON protocol that connects server-side component builders to client-side renderers. Both the TypeScript and Python libraries produce this format.

Note: This TypeScript library is a superset of the Python prefab-ui (v0.20.x). Core components and the v0.3 protocol are identical. Chart formatting props (xAxisFormat, tooltipXFormat, tooltipXKey, per-series tooltipFormat, dual Y-axis) are TS-only extensions. The renderer handles both payloads seamlessly, and still accepts legacy 0.2 payloads.

Envelope ​

Every prefab UI is wrapped in a top-level envelope:

json
{
  "$prefab": { "version": "0.3" },
  "view": { ... },
  "state": { ... },
  "css": [ ... ],
  "stylesheets": [ ... ],
  "mode": "dark",
  "defs": { ... },
  "keyBindings": { ... },
  "onMount": { ... }
}
FieldTypeRequiredDescription
$prefab{ version: string }YesFormat identifier. Current version: "0.3"
viewComponentJSONYesRoot component tree
stateRecord<string, unknown>NoInitial reactive state
cssstring[]NoInline CSS blocks injected as <style> tags. The theme is compiled into this array (:root for light, .dark, [data-theme="dark"] for dark).
stylesheetsstring[]NoExternal CSS URLs loaded as <link rel="stylesheet">
mode"light" | "dark"NoForce a color scheme regardless of OS preference
defsRecord<string, ComponentJSON>NoReusable component templates
keyBindingsRecord<string, ActionJSON>NoKeyboard shortcut → action
onMountActionJSON | ActionJSON[]NoAction(s) to run when the UI renders

Protocol 0.3 (upstream PR #431). The theme moved off the wire: instead of a structured theme object, PrefabApp.toJSON() compiles it into the css array and emits mode. stylesheets now means external URLs. The renderer still accepts 0.2 payloads (a structured theme object, and inline CSS in stylesheets) for backward compatibility.

Component JSON ​

Every component serializes to a flat JSON object:

json
{
  "type": "Button",
  "label": "Click me",
  "variant": "default",
  "size": "sm",
  "onClick": { "action": "setState", "key": "count", "value": "{{ count + 1 }}" }
}
FieldTypeDescription
typestringRequired. Component type name (PascalCase)
contentstringText content (supports {{ }} expressions)
childrenComponentJSON[]Child components
cssClassstringExtra CSS class
*unknownAny other props specific to the component type

Type Names ​

Component types use PascalCase and map 1:1 to the TypeScript/Python function names:

Layout:       Column, Row, Grid, GridItem, Container, Div, Span,
              Dashboard, DashboardItem, Pages, Page, MasterDetail, Detail
Typography:   Heading, H1, H2, H3, H4, Text, P, Lead, Large, Small, Muted,
              BlockQuote, Label, Link, Code, Markdown, Kbd
Card:         Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter
Data:         DataTable, Badge, Dot, Metric, Ring, Progress, Separator, Loader, Icon
Table:        Table, TableHead, TableBody, TableFooter, TableRow,
              TableHeader, TableCell, TableCaption, ExpandableRow
Form:         Form, Input, Textarea, Button, ButtonGroup,
              Select, SelectOption, SelectGroup, SelectLabel, SelectSeparator,
              Checkbox, Switch, Slider, Radio, RadioGroup,
              Combobox, ComboboxOption, ComboboxGroup, ComboboxLabel, ComboboxSeparator,
              Calendar, DatePicker, Field, FieldTitle, FieldDescription,
              FieldContent, FieldError, ChoiceCard
Interactive:  Tabs, Tab, Accordion, AccordionItem, Dialog, Popover, Tooltip,
              HoverCard, Carousel
Charts:       BarChart, LineChart, AreaChart, PieChart, RadarChart, ScatterChart,
              Sparkline, RadialChart, Histogram
Media:        Image, Audio, Video, Embed, Svg, DropZone, Mermaid, PdfViewer
Alert:        Alert, AlertTitle, AlertDescription
Control:      ForEach, If, Elif, Else, Condition, Define, Use, Slot

Action JSON ​

Actions are serialized as objects with an action field:

json
{ "action": "setState", "key": "count", "value": 42 }
json
{ "action": "toolCall", "tool": "get_data", "arguments": { "id": "{{ selectedId }}" } }
json
{ "action": "showToast", "message": "Saved!", "variant": "success" }

Action Types ​

actionDescriptionKey Fields
setStateSet state valuekey, value
toggleStateToggle booleankey
appendStateAppend to arraykey, value, index?
popStateRemove from arraykey, index
showToastToast notificationmessage, description?, variant?, duration?
closeOverlayClose dialog/popover—
openLinkNavigate to URLurl, target?
setIntervalPeriodic timerintervalMs, onTick
fetchHTTP requesturl, method?, headers?, body?, resultKey?
openFilePickerFile pickeraccept?, multiple?, resultKey?
callHandlerClient handlerhandler, arguments?, resultKey?
toolCallMCP tool calltool, arguments?, resultKey?
sendMessageChat messagemessage
updateContextHost context updatecontext
requestDisplayModeDisplay modemode
subscribeResource subscription, polling fallbackuri, stateKey, push?, fallbackInterval?, fallbackTool?, fallbackArgs?, onData?
unsubscribeEnd a subscriptionuri

All actions support optional onSuccess and onError callbacks (single action or array). The renderer also accepts callTool as an alias of toolCall.

Reactive Expressions ​

String values containing {{ }} are evaluated as reactive expressions:

json
{ "type": "Text", "content": "{{ count }}" }
{ "type": "Text", "content": "Hello, {{ name | upper }}!" }

Expression Grammar ​

expression   = ternary | logical
ternary      = logical "?" value ":" value
logical      = comparison (("&&" | "||") comparison)*
comparison   = additive (("===" | "!==" | ">" | ">=" | "<" | "<=") additive)?
additive     = multiplicative (("+" | "-") multiplicative)*
multiplicative = unary (("*" | "/" | "%") unary)*
unary        = "!" primary | primary
primary      = number | string | boolean | null | dotpath | "(" expression ")"
dotpath      = identifier ("." identifier)*
piped        = expression ("|" pipeName (":" pipeArg ("," pipeArg)*)? )*

Pipes ​

PipeSyntaxDescription
upper / lowername | upperChange case
truncatedesc | truncate:50Cut to N characters
currencyprice | currency:'EUR'Format as currency (default USD)
numbervalue | number:2Number, optionally fixed to N decimals
percentrate | percent:1Ratio as a percentage
compactcount | compactCompact number (1.2K)
roundvalue | round:2Round to N decimals
absdelta | absAbsolute value
date / time / datetimets | dateLocale date, time, or both
lengthitems | lengthArray or string length
first / lastitems | firstFirst or last element
jointags | join:', 'Join an array
defaultname | default:'N/A'Fallback for null, undefined, or ''
pluralizecount | pluralize:'item'The word, with s unless the value is 1
selectattr / rejectattrrows | selectattr:'active'Keep rows whose field is truthy / falsy
findcollection | find:'keyField',stateKeyRefFind a row where row[keyField] matches the value of stateKeyRef
dotobject | dot:'field'Extract a property from an object

Pipes chain left to right: {{ patients | find:'id',selectedPatientId | dot:'name' }}. find and dot carry the Signal/Collection/Ref data pattern on the wire. A wire or custom pipe cannot shadow a built-in name.

Scope Variables ​

VariableAvailable InDescription
key.pathEverywhereState value, by dot path (no state. prefix)
$itemForEach bodyCurrent iteration item
$indexForEach bodyCurrent iteration index
$eventonChangeThe new value
$erroronError callbackError value
$resultonSuccess callbackAction result

In onSubmit, each form field is available by its name.

Theme (authoring input → compiled css) ​

You author a theme as a { light, dark } map of CSS custom properties:

ts
new PrefabApp({
  view,
  theme: {
    light: { primary: '#3b82f6', background: '#ffffff', foreground: '#0a0a0a' },
    dark:  { primary: '#60a5fa', background: '#0a0a0a', foreground: '#fafafa' },
  },
})

In protocol 0.3 this is compiled into the wire css array (it is not sent as a structured theme object). Light variables target :root; dark variables target .dark, [data-theme="dark"] plus a prefers-color-scheme: dark block:

json
{
  "$prefab": { "version": "0.3" },
  "view": { ... },
  "css": [
    ":root {\n  --primary: #3b82f6;\n  --background: #ffffff;\n}\n.dark, [data-theme=\"dark\"] {\n  --primary: #60a5fa;\n}"
  ]
}

The renderer injects each css entry as a <style> tag. Color scheme is selected by the host's prefers-color-scheme, the data-theme attribute, or the dark/light class. Use the top-level mode field to force one. (Legacy 0.2 payloads carrying a structured theme object still render.)

State Updates (Partial) ​

For incremental updates without re-rendering the full UI, use a state update wire:

json
{
  "$prefab": { "version": "0.3" },
  "stateUpdate": {
    "count": 42,
    "status": "complete"
  }
}

This merges values into the existing state store and triggers re-evaluation of affected reactive expressions.

Validation ​

Use validateWireFormat() to check a payload:

ts
import { validateWireFormat } from '@maxhealth.tech/prefab'

const result = validateWireFormat(jsonPayload)
if (!result.valid) {
  console.error(result.errors)
  // [{ path: '$.view', message: 'Missing required "view" field' }]
}

Released under the MIT License.