---
url: /prefab/guide/getting-started.md
description: >-
Install @maxhealth.tech/prefab and build your first MCP App UI in minutes.
Covers npm setup, display() helpers, and browser rendering.
---
# Getting Started
## Installation
```bash
npm install @maxhealth.tech/prefab
# or
bun add @maxhealth.tech/prefab
```
## Base Theme CSS
Prefab ships a base CSS theme (`prefab.css`) that provides design tokens and structural styles for all components.
**Bundler (Vite / webpack):**
```ts
import '@maxhealth.tech/prefab/prefab.css'
```
**CDN:**
```html
```
When using `toHTML()`, the base CSS is injected automatically. Pass `{ includeStyles: false }` to opt out.
The layering order is: `prefab.css` (base) → `stylesheets[]` (your overrides) → `theme` (runtime CSS variables).
### Token values are host-adaptive
Each token in `prefab.css` is a fallback chain rather than a literal:
```css
--background: var(--color-background-primary, var(--vscode-editor-background, #ffffff));
```
MCP Apps host variables win, then VS Code webview variables, then the static
default. That is what makes a viewer inherit the surrounding editor or client
theme without any configuration, so avoid replacing these with flat values
unless you genuinely want to override the host.
### Driving prefab from a shared brand
The token *names* are the [brandc](https://www.npmjs.com/package/brandc) contract,
the same vocabulary the other Max Network kits read. If you author a brand as
data, `toPrefabTheme()` emits exactly prefab's wire `theme` shape, so no adapter
is needed:
```ts
import { toPrefabTheme, dashboard } from 'brandc'
import { display, autoTable } from '@maxhealth.tech/prefab'
return display(autoTable(rows), { theme: toPrefabTheme(dashboard) })
```
prefab takes no runtime dependency on brandc (its `dependencies` are empty by
design, and the CDN renderer bundle must stay that way). brandc is a
devDependency used by `test/brand-contract.test.ts`, which fails if prefab ever
introduces a token outside the shared contract or if a brand stops covering a
token prefab reads.
## Usage Modes
prefab has four usage modes:
| Mode | Where | Import |
|------|-------|--------|
| **Server-side** | MCP tool handlers (Python/TS) | `@maxhealth.tech/prefab` |
| **Client-side** | Browser (MCP Apps iframe) | `dist/renderer.min.js` script tag |
| **Hybrid** | Node/Bun backend → HTML response | `PrefabApp.toHTML()` |
| **Remote** | Any MCP client (VS Code, Claude, etc.) | See [Remote section](#remote-use-the-hosted-mcp-server) |
::: tip See it in action
The [interactive demo](/demo/) shows how an LLM prompt becomes a fully rendered UI — dashboards, forms, charts, and more — powered by the client-side renderer.
:::
***
## Server-Side: Build UIs in MCP Tool Handlers
Build a component tree, wrap it with `display()`, and return it as an MCP tool result.
```ts
import {
display, Column, H1, Text, DataTable, col, Badge, autoTable,
} from '@maxhealth.tech/prefab'
// Simple: auto-generate a table from data
async function listUsers() {
const users = await db.query('SELECT * FROM users')
return display(autoTable(users), { title: 'Users' })
}
// Advanced: hand-craft the layout
async function userDashboard() {
const users = await db.query('SELECT * FROM users')
return display(
Column({
gap: 8,
children: [
H1('User Dashboard'),
Text('Manage your organization members.'),
DataTable({
rows: users,
columns: [
col('name', 'Name'),
col('email', 'Email'),
col('role', 'Role'),
col('status', 'Status'),
],
search: true,
}),
],
}),
{ title: 'User Dashboard' },
)
}
```
The `display()` function serializes the tree to `$prefab` wire JSON and wraps it in an MCP tool result content array. Any MCP client that understands prefab can render it.
## Client-Side: Browser ext-app
Load the renderer bundle and use the `app()` factory. See the [live demo](/demo/) for a complete working example.
```html
My App
```
::: tip Use versioned CDN URLs
Always pin a version (e.g. `@0.3`) in production to prevent breaking changes.
:::
The `app()` factory:
1. Detects whether the page is in an iframe (bridge mode) or standalone
2. Performs the PostMessage handshake with the host (if bridged)
3. Applies the host theme
4. Returns an API object with `callTool`, `render`, `onToolInput`, etc.
## Hybrid: Self-Contained HTML
Use `PrefabApp.toHTML()` to generate a complete HTML page from a server:
```ts
import { PrefabApp, Column, H1, Text } from '@maxhealth.tech/prefab'
const app = new PrefabApp({
title: 'My Dashboard',
view: Column({ gap: 4, children: [H1('Hello'), Text('World')] }),
})
const html = app.toHTML()
// Returns a self-contained HTML page with embedded JSON + renderer script
```
Options:
| Option | Default | Description |
|--------|---------|-------------|
| `cdnVersion` | Current package version | CDN version for script/CSS tags |
| `pretty` | `false` | Pretty-print the embedded JSON |
| `includeStyles` | `true` | Inject the `prefab.css` base theme |
## Remote: Use the Hosted MCP Server
The fastest way to use prefab — no installation needed. Point any MCP client at the hosted renderer server:
> **Important:** Claude Code / Claude Desktop require `/mcp` as the
> first path segment. VS Code accepts any path. Use the appropriate
> URL for your client.
**VS Code (`settings.json` or `.vscode/mcp.json`):**
```json
{
"servers": {
"prefab-renderer": {
"type": "http",
"url": "https://maxhealth.tech/prefab/mcp"
}
}
}
```
**Claude Desktop / Claude Code (`claude_desktop_config.json`):**
```json
{
"mcpServers": {
"prefab-renderer": {
"type": "http",
"url": "https://maxhealth.tech/mcp/prefab"
}
}
}
```
Once connected, the server exposes a `render_prefab_ui` tool that accepts `$prefab` wire-format JSON and returns rendered HTML. Your LLM can call it directly to produce rich UI from structured data.
::: tip See it in action
The [interactive demo](/demo/) shows exactly what the remote renderer produces — dashboards, forms, charts, and more.
:::
## Subpath Imports
```ts
import { ... } from '@maxhealth.tech/prefab' // Everything
import { ... } from '@maxhealth.tech/prefab/actions' // Actions only
import { ... } from '@maxhealth.tech/prefab/rx' // Rx expressions
import { ... } from '@maxhealth.tech/prefab/charts' // Chart components
import { ... } from '@maxhealth.tech/prefab/mcp' // MCP display helpers
import { ... } from '@maxhealth.tech/prefab/renderer' // Browser renderer
import '@maxhealth.tech/prefab/prefab.css' // Base theme CSS
```
## Next Steps
* [Live Demo](/demo/) — see LLM prompts rendered as live UIs
* [Components](./components) — the component model, with the [full catalog](/reference/components) of all 115+ components
* [Actions](./actions) — client-side and MCP actions ([API](/reference/api/actions/))
* [Reactive Expressions](./rx) — dynamic values and [Signals & Collections](./rx#signals--collections) ([API](/reference/api/rx/))
* [Auto-Renderers](./auto-renderers) — generate UIs from raw data ([API](/reference/api/auto/))
* [Wire Format](/reference/wire-format) — the `$prefab` JSON spec
---
---
url: /prefab/guide/components.md
description: >-
Guide to prefab's 115+ declarative UI components — layout, typography, forms,
data tables, charts, media, and interactive elements.
---
# Components
A prefab UI is just data. You describe what you want as a tree of typed nodes, and the renderer turns that tree into a live interface. There is no JSX, no virtual DOM to reason about, and no framework to learn — only a vocabulary of component functions that you nest inside one another.
## The mental model
Each component is a function that returns a `Component` instance. Calling it builds one node in the tree; passing other components as its children grows the tree downward. When you are ready to ship, the whole structure serializes to plain `$prefab` wire JSON, which any prefab-aware client can render.
Most components share a friendly shape: an optional **props** object first, then an array of **children**.
```ts
Column({
gap: 6,
children: [
H1('Patients'),
Text('Everyone under your care, at a glance.'),
],
})
```
Containers like `Column` and `Row` hold other nodes; leaf nodes like `Text` and `Badge` carry content. Containers take a single props object, with the child nodes under `children`. Leaf nodes take their content positionally, which is why `H1('Patients')` and `Text('…')` read the way they do.
## How components compose
Composition is the whole game. You build small, meaningful pieces and slot them into larger layouts, the same way you would nest boxes inside boxes. A card lives inside a column, a table lives inside a card, a badge lives inside a table cell. Because every node is data, you can also build trees programmatically — map over rows, conditionally include a section, or factor a repeated fragment into a helper function.
Props that drive behavior (an `onClick` action, a reactive `cssClass`, a `from` collection) make these static trees come alive without changing the composition model.
## The categories
prefab groups its 115+ components into a handful of families. Reach for the family that matches your intent:
* **Layout** — structural containers that control direction, spacing, and grids (`Column`, `Row`, `Grid`, `MasterDetail`).
* **Typography** — headings, paragraphs, labels, and inline text styles.
* **Card & Alert** — grouped content surfaces and status callouts.
* **Forms** — inputs, selects, checkboxes, date pickers, and field wrappers.
* **Data display** — tables, badges, metrics, and progress indicators.
* **Charts** — bar, line, area, pie, radar, scatter, and more, rendered natively as SVG.
* **Media** — images, audio, video, embeds, and file drop zones.
* **Interactive** — tabs, accordions, dialogs, popovers, and carousels.
* **Control flow** — `If`/`Else`, `ForEach`, and reusable `Define`/`Use` templates that shape the tree at render time.
Pick a container, fill it with the leaves and nested containers you need, and let the renderer handle the rest.
→ See the [Components reference](/reference/components) for the full catalog: every component, its props, and examples.
---
---
url: /prefab/guide/actions.md
description: >-
How to attach actions to components — CallTool, SetState, ShowToast, Navigate,
and other serializable event handlers for MCP Apps.
---
# Actions
Actions are how a prefab UI does something. They are small, serializable commands you attach to a component event — `onClick`, `onChange`, `onSubmit`, or the app-level `onMount`. Because they are plain data (not closures), they travel across the wire and run wherever the renderer lives, which is what makes interactivity possible in an MCP App.
## Two execution models
Every action falls into one of two camps:
* **Client-side** actions resolve entirely in the renderer with no server roundtrip. Setting state, toggling a flag, showing a toast, opening a link, or picking a file all happen instantly in the browser.
* **MCP transport** actions take a roundtrip through the host — calling a tool, sending a chat message, or updating shared context. These are how your UI reaches back into the server that produced it.
Reaching for the right camp is usually obvious: if the work is purely visual or local, keep it client-side; if it needs the model or your backend, call a tool.
## Chaining with onSuccess and onError
Most actions accept `onSuccess` and `onError` options, each taking a single action or an array of them. This lets you compose flows declaratively — save a record, then flip a flag and show a success toast, and surface an error toast if it fails — without writing any imperative glue.
```ts
Button('Save & Notify', {
onClick: new CallTool('save_item', {
arguments: { name: STATE.name },
onSuccess: [new SetState('saved', true), new ShowToast('Saved!', { variant: 'success' })],
onError: new ShowToast('Save failed', { variant: 'error' }),
}),
})
```
## Lifecycle: running on mount
Actions are not limited to user gestures. Pass `onMount` to `display()` to run an action the moment the UI first renders — perfect for loading initial data or opening a real-time subscription so the view arrives already populated.
## When to use the builder sugar
For the state-mutating actions (`set`, `toggle`, `append`, `pop`), prefab ships ergonomic wrappers that accept a `Signal` or `Collection` instead of a raw string key. Prefer them whenever you already model your state with signals: you keep type safety, avoid stringly-typed keys, and produce the exact same wire-format actions. Drop down to the raw action classes when you need a one-off or are working without a signal.
→ See the [Actions API reference](/reference/api/actions/) for every action class, its constructor options, and serialized shape.
---
---
url: /prefab/guide/rx.md
description: >-
Reactive template expressions, pipes, signals, and collections — prefab's
client-side state system with auto-updating UI bindings.
---
# Reactive Expressions (`rx`)
A reactive expression is just a string wrapped in `{{ }}` that the renderer keeps alive. Instead of baking a value into your UI once, you hand the renderer a tiny formula. Whenever the underlying state changes, the renderer re-runs the formula and patches only the affected parts of the screen.
```
Hello, {{ user.name }}! → Hello, Ada!
```
Change `user.name` in state and the greeting updates on its own. No re-render call, no event wiring — that is the whole idea.
## The mental model
Think of four moving pieces that flow in one direction:
1. **State store** — a plain key/value bag that lives in the browser (`count`, `user`, `patients`, …).
2. **Expressions** — `{{ }}` formulas that read from that store and from loop or event scope.
3. **Pipes** — `| filters` that format a value for display (`{{ price | currency:'EUR' }}`).
4. **Auto-update** — when state changes, every expression that touched the changed key re-evaluates and the DOM follows.
You rarely write the raw strings by hand. The `rx()` builder gives you a typed, chainable way to compose them, so a typo becomes a compile error instead of a blank screen:
```ts
import { rx, STATE } from '@maxhealth.tech/prefab'
rx('count').add(1) // → "{{ count + 1 }}"
rx('name').upper() // → "{{ name | upper }}"
rx('active').then('On', 'Off')
```
Expressions can also reference scope that only exists in certain places — the current `item` and `index` inside a `ForEach` loop, the `event` payload on a form, or the `result` and `error` of a tool call. Each has a matching builder (`ITEM`, `INDEX`, `EVENT`, `RESULT`, `ERROR`).
## Signals & Collections
Raw state keys work, but they are untyped and easy to misspell. **Signals** and **collections** are a thin typed layer on top of the same state store, giving your reactive data a name, a shape, and a home.
* A **signal** is a single reactive scalar — a string, number, boolean, or null. Think "the currently selected id" or "is the panel open".
* A **collection** is a keyed array of rows, with helpers to look a row up by a signal's value.
The nice part: declaring them is enough. Their initial values are auto-collected into `PrefabApp` state, so you never hand-assemble a `state: { ... }` object.
```ts
import { signal, collection } from '@maxhealth.tech/prefab'
const patients = collection('patients', data, { key: 'id' })
const selectedId = signal('selectedPatientId', patients.firstKey())
// patients.by(selectedId) → "{{ patients | find:'id',selectedPatientId }}"
// Reads the selected row, reactively. Bind it straight into a component.
```
Because everything compiles down to `{{ }}` expressions, a signal change ripples through every binding that depends on it — exactly like a hand-written expression, just type-safe.
→ See the [Rx API reference](/reference/api/rx/) for the full API: `rx`, `signal`, `collection`, every scope variable, and the pipe registry.
---
---
url: /prefab/guide/auto-renderers.md
description: >-
Auto-generate tables, charts, forms, metrics, and timelines from raw data with
autoTable, autoChart, autoForm, and autoMetrics.
---
# Auto-Renderers
Most of the time you already have the data — a list of users from a query, a few KPIs, a row of monthly sales. What you *don't* want to do is hand-wire a table, pick column definitions, choose a chart type, and style badges every single time. Auto-renderers close that gap: hand them raw data, get a finished UI back in one call.
## The Mental Model
Think of an auto-renderer as a single function with a simple shape: **data in → component tree out.**
```ts
const users = await db.query('SELECT * FROM users')
return display(autoTable(users), { title: 'Users' })
```
That `autoTable(users)` call inspects your data, infers the columns from the object keys, detects which fields are statuses (and renders them as badges), and returns a normal `Component`. Because the output is just a regular component, it slots into any layout — wrap it in a `Column`, drop it next to a chart, or hand it to `display()`. There is no special runtime and no escape hatch to learn.
The family follows one consistent convention: the **first argument is your data**, and an **optional second argument is an options object** (title, layout hints, behaviour flags). `autoForm` is the small exception — it takes a submit-tool name in the middle so a generated form knows which MCP tool to call.
A useful way to picture the whole family is by the shape of input each one expects. Roughly:
* a **single object** describes one thing → a detail view,
* a **list of records** describes many of the same thing → a table, chart, or comparison,
* a **sequence in time** describes a journey → a timeline or progress tracker,
* a **set of fields** describes an intent to capture → a form.
Once you internalise that mapping, choosing the right helper rarely needs the reference at all — you reach for the one whose input shape matches the data already in your hand.
## When to Reach for Them
Auto-renderers shine when the *structure* of your UI mirrors the *structure* of your data:
* An array of records → a searchable, sortable table.
* A handful of numbers → a row of metric cards.
* Date-stamped events → a vertical timeline.
* A list of fields → a working form wired to a tool.
They are perfect for MCP tool handlers, where you frequently turn an API response straight into something renderable with minimal ceremony.
## From a Schema, Not From a Field List
`autoForm` takes a list of fields, and writing that list is usually restating something the server already knows. An MCP tool declares an `inputSchema`. A REST route declares a request body. Both are JSON Schema, and `fieldsFromJsonSchema()` turns one into the field list:
```ts
const fields = fieldsFromJsonSchema({
type: 'object',
required: ['email'],
properties: {
email: { type: 'string', format: 'email', title: 'Email' },
plan: { type: 'string', enum: ['pro', 'team'] },
seats: { type: 'integer', minimum: 1, maximum: 50 },
},
})
return display(autoForm(fields, 'create_account', { title: 'New account' }))
```
The form now asks for exactly what the tool accepts, because it is derived from the same declaration the tool validates against. Formats become the right control — `email`, `uri`, `date-time`, `password` — an enum becomes a Select, an array of enums becomes a multi-select, and `minimum` / `maxLength` carry over as bounds.
It only emits what a flat form can honestly ask for. A nested object, an array of objects, or a property marked `readOnly` is skipped rather than rendered as a control that cannot round-trip. Pass `include` to fix the order, or `exclude` to drop keys the caller already knows.
This is the inverse of `formSchema()`, which derives an elicitation schema from a field list for hosts with no UI. Together they mean one declaration serves both paths: the schema draws the form, and the form's fields describe the schema.
## When to Hand-Craft Instead
Reach for the underlying components directly when you need precise control — bespoke column renderers, custom interactions, conditional layouts, or a design that doesn't map cleanly onto your raw data shape. Auto-renderers are a fast on-ramp, not a ceiling: you can start with `autoTable`, then graduate to a hand-built `DataTable` the moment you need something the auto path can't express. The two styles compose freely in the same view.
→ See the [Auto-Renderers API reference](/reference/api/auto/) for the full API: every helper, its options, and examples.
---
---
url: /prefab/guide/mcp-display.md
description: >-
display(), displayForm(), rendererHtml(), and registerViewerResource() —
server-side helpers to return prefab UIs from MCP tools.
---
# MCP Display Helpers
When an MCP tool runs, it hands back a result. The display helpers let that
result be a living UI instead of a wall of text. You build a component tree on
the server, hand it to a helper, and the helper packages it into the exact
shape an MCP host expects to render.
## The mental model
Think of every tool handler as three small steps:
1. **Build** — compose your screen from prefab components (a `Column`, a table,
a form, a chart).
2. **Wrap** — pass that tree to a display helper. The helper serializes it to
the `$prefab` wire format and folds it into an MCP tool-result envelope.
3. **Return** — hand the envelope straight back from your handler. Any host that
speaks prefab paints it.
```ts
import { display, Column, H1, autoTable } from '@maxhealth.tech/prefab'
async function listPatients() {
const patients = await db.query('SELECT * FROM patients')
return display(Column({ children: [H1('Patients'), autoTable(patients)] }), { title: 'Patients' })
}
```
You never touch the envelope by hand. The helper knows where the JSON goes,
which fields the host reads, and how to keep older and newer hosts happy.
## Full display vs. incremental update
There are two ways to send something back, and picking the right one keeps your
UIs snappy.
Reach for a **full display** when the screen changes shape — a new list, a
detail page, a form, an error or success card. The host swaps in the whole tree
and renders it fresh.
Reach for an **incremental update** when the screen is already on the user's
screen and you only need to nudge some values inside it — a counter ticking up,
a status flipping to "done", a freshly fetched total. Instead of re-sending the
entire view, you send just the changed state, and the renderer merges it into
the live store without rebuilding anything.
That split is the whole game: send a full view when the layout changes, send a
patch when only the data does.
## Chaining tools
Because each handler returns a self-contained UI, tools compose naturally. A
list view can carry a button whose click calls a detail tool; the detail view
can carry one that opens an edit form; submitting the form calls a save tool.
Each step is just another handler returning its own `display(...)`, so rich,
multi-screen flows fall out of simple pieces.
→ See the [MCP API reference](/reference/api/mcp/) for the full API: every helper, its options, and the tool-result shape.
---
---
url: /prefab/guide/input-required.md
description: >-
Ask the user for input under MCP 2026-07-28 — display_form with elicit,
formInputRequest, and reading the answer with acceptedFormInput.
---
# Asking for input
Protocol revision 2026-07-28 made the MCP core stateless and removed
server-initiated `elicitation/create` pushes. A handler now asks for input by
**returning** an `input_required` result: the client collects the answers and
retries the original call, and the handler runs again with the answers in hand.
That matters to `display_form()`. A prefab form is a UI, and a UI only exists on
a host that renders one. On a host with no MCP Apps surface the form was
unreachable. The same `AutoFormField[]` now also derives the restricted
elicitation schema, so one field list serves both paths.
## The two paths
```ts
const fields = [
{ name: 'email', label: 'Email', type: 'email', required: true },
{ name: 'plan', label: 'Plan', options: [{ value: 'pro', label: 'Pro' }, { value: 'team' }] },
]
// A host that renders UI: a prefab form calling the `signup` tool on submit.
display_form(fields, 'signup', { title: 'Create your account' })
// Any host: an input_required result the client fills in natively.
display_form(fields, 'signup', { title: 'Create your account', elicit: true })
```
Both ask for exactly the same thing, because both are derived from `fields`.
## The write-once handler
Write one handler that runs on every round: read the answers first, request only
what is still missing, and return the real result once everything has arrived.
```ts
const FIELDS = [
{ name: 'email', label: 'Email', type: 'email', required: true },
{ name: 'plan', label: 'Plan', options: [{ value: 'pro' }, { value: 'team' }] },
]
async function signup(_args: unknown, ctx: { inputResponses?: McpInputResponses }) {
const answers = acceptedFormInput(ctx.inputResponses, 'signup', FIELDS)
if (answers == null) {
return formInputRequest(FIELDS, { key: 'signup', message: 'Create your account' })
}
return display(autoDetail(await createAccount(answers)))
}
```
Round one finds no answers and returns the request. The client shows its form
and retries. Round two finds the answers and the handler finishes.
## Reading the answer
`inputResponses` comes from the client, so treat it as untrusted.
`acceptedFormInput` checks it against the same field list that produced the
schema: unknown keys are dropped, values of the wrong type or outside the
advertised bounds are dropped, an enum value that was never offered is dropped,
and a missing required field fails the whole answer.
```ts
const answers = acceptedFormInput(responses, 'signup', FIELDS)
```
It returns `undefined` for a missing, declined, or cancelled answer alike.
Re-requesting is the right move for all three only when the request is
idempotent. When a refusal has to be told apart from a first pass, read the
response directly:
```ts
const view = inputResponse(responses, 'signup')
if (view?.action === 'decline') {
// The user said no. Asking again is not the answer.
}
```
## The schema
`formSchema(fields)` produces the restricted JSON Schema on its own, which is
useful when the elicitation is issued through the SDK rather than through
prefab:
```ts
const schema = formSchema([
{ name: 'email', type: 'email', required: true },
{ name: 'age', type: 'number', min: 18, max: 120 },
{ name: 'tags', options: [{ value: 'a' }, { value: 'b' }], multiple: true },
])
```
The wire accepts a flat object of primitives and nothing else, so the mapping is
deliberately narrow:
| Field | Schema |
|---|---|
| `type: 'email' \| 'url' \| 'date' \| 'datetime'` | `string` with the matching `format` |
| `type: 'number' \| 'integer' \| 'range'` | `number` / `integer`, `min`/`max` as bounds |
| `type: 'checkbox' \| 'boolean'` | `boolean` |
| `options` | `string` with `enum` |
| `options` + `multiple` | `array` of `string` with `enum` items |
| anything else | `string`, `min`/`max` as length bounds |
`required: true` puts the field in the schema's `required` list. `label` becomes
the `title`, `description` becomes the `description`, and `default` is carried
across when its type matches.
A `password` field produces a plain string. The restricted schema has no
secret-input format, and claiming one would misrepresent how the client is going
to render it.
## Nesting and richer shapes
The restricted subset has no nested objects and no arrays beyond multi-select
enums. A flow that genuinely needs a richer shape belongs on the UI path, where
prefab renders the whole form and submits it through `CallTool`, or in several
rounds carrying `requestState` between them:
```ts
formInputRequest(FIELDS, { key: 'signup', requestState: signedToken })
```
`requestState` round-trips through the client and comes back as
attacker-controlled input. Sign it — the MCP TypeScript SDK's
`createRequestStateCodec` gives you an HMAC `{ mint, verify }` pair — and mint
only what earlier rounds already proved.
---
---
url: /prefab/guide/a2ui.md
description: >-
Emit A2UI from a prefab component tree — toA2UI(), the a2ui:// resource
helper, the Basic-catalog mapping table, and what degrades on the way across.
---
# A2UI
[A2UI](https://a2ui.org) is the declarative agent-to-UI protocol: an agent sends
JSON describing a component tree plus a data model, and the renderer maps the
abstract component names onto its own widgets — React, Angular, Lit, Flutter,
Swift, Jetpack Compose.
prefab speaks it as a second output target. The same server-side component tree
that produces a `$prefab` payload also produces A2UI messages, so one authoring
API reaches both:
| | `$prefab` | A2UI |
|---|---|---|
| Rendered by | prefab's own renderer | the host's A2UI renderer |
| Delivery | MCP Apps `ui://` HTML resource, or any web page | `a2ui://` resource or an embedded resource in a tool result |
| Surface | sandboxed iframe | native widgets, no iframe |
| Catalog | 115+ components | the 18-component Basic catalog |
## Emitting
```ts
const app = new PrefabApp({
view: Column({ children: [H1('Users'), autoTable(rows)] }),
})
const { messages, diagnostics } = app.toA2UI()
```
`messages` is a list of A2UI protocol messages. By default everything is inlined
into a single `createSurface`, which is what a stored payload wants. Pass
`{ stream: true }` to split it into `createSurface` + `updateComponents` +
`updateDataModel` so a streaming transport can paint early.
`diagnostics` is the part worth reading. prefab has 115+ components and the
Basic catalog has 18, so some of the tree changes shape on the way across, and
every change is reported:
```ts
for (const d of app.toA2UI().diagnostics) {
console.warn(`${d.kind}: ${d.subject} — ${d.detail}`)
}
```
| Kind | Meaning |
|---|---|
| `degraded` | Rendered as something simpler. An `Alert` became a `Card`, a `Metric` became a `Column` of `Text`. |
| `unsupported` | Dropped. Charts, diagrams and file uploads have no Basic-catalog reading. |
| `expression` | A `{{ }}` template was richer than a JSON Pointer, so the binding could not be made. |
| `action` | An action had no direct equivalent and was reported to the agent as an event instead. |
## Serving it over MCP
Return a surface from a tool with `display_a2ui`. The payload travels as an
embedded resource under the `application/a2ui+json` MIME type, which is how a
host knows to route it to its A2UI renderer:
```ts
server.registerTool('list-users', schema, async () =>
display_a2ui(autoTable(await db.users())))
```
For a surface that does not depend on the conversation, register it as a
resource instead — the host reads it once and caches it:
```ts
registerA2uiResource(server, () => Column({ children: [
H1('Settings'),
Input({ name: 'apiKey', label: 'API key' }),
] }), { uri: 'a2ui://myserver/settings' })
```
The builder runs on every read, so a surface closing over live data refreshes
without re-registering. Caching is off by default for that reason; pass
`cache: { ttlMs, cacheScope }` when the surface really is static.
Both live alongside the MCP Apps path in [MCP Apps](/reference/mcp-apps) — a
server can offer `ui://` and `a2ui://` from the same tool and let the host pick.
## In the browser
The emitter also ships as a standalone IIFE bundle, apart from
`renderer.min.js`. Almost no page that renders `$prefab` also emits A2UI, so
folding the two together would tax every consumer for a feature they do not use:
```html
```
`emit` takes parsed `$prefab` JSON rather than a component tree, so nothing in
the bundle needs the authoring API. That is what keeps it around a sixth the
size of the renderer.
The [playground](https://maxhealth.tech/prefab/playground/) runs this bundle:
switch the preview pane to **A2UI** to see any payload translated live, with the
diagnostics listed underneath.
## How the tree crosses over
Two structural differences drive everything:
**Flat, not nested.** A2UI components live in an adjacency list. Every component
carries an `id` and parents reference children by id. prefab's nested `children`
are flattened, ids are allocated deterministically in traversal order, and the
entry component is named `root` as the protocol requires. An `id` you set
yourself is honoured.
**Bound, not interpolated.** A2UI reads dynamic values through JSON Pointer
bindings. `{{ user.name }}` becomes `{ "path": "/user/name" }`, and prefab's
`state` becomes the surface data model.
Text that mixes literals with values goes through the `formatString` catalog
function, so `Score: {{ score }}` becomes
`{ call: 'formatString', args: { value: 'Score: ${/score}' } }` rather than being
lost. What has no equivalent is arithmetic, pipes and conditionals —
`{{ count + 1 }}`, `{{ price | currency:'USD' }}` — and those raise an
`expression` diagnostic. A string is interpolated only if *every* value in it
binds; one unbindable expression makes the whole string unbindable, because
interpolating half of it would change what the text says without saying so.
### Pipes
A2UI has no expression language, but its catalog has the formatting functions
that prefab's common pipes correspond to, so seven of the twenty-two map:
| prefab | A2UI |
|---|---|
| `currency` | `formatCurrency(value, currency)` |
| `number`, `round` | `formatNumber(value, decimals)` |
| `date`, `time`, `datetime` | `formatDate(value, format)` |
| `pluralize` | `pluralize(value, one, other)` |
`{{ price | currency:'EUR' }}` becomes
`{ call: 'formatCurrency', args: { value: { path: '/price' }, currency: 'EUR' } }`.
The date pipes are the one inexact mapping. prefab renders through the reader's
locale; A2UI's `formatDate` requires an explicit Unicode TR35 pattern, so one is
chosen and a `degraded` diagnostic says which. Losing the value entirely would be
the worse trade.
The other fifteen — `truncate`, `join`, `selectattr`, `percent`, `compact` and
friends — transform data rather than format it, and stay reported. So does a
chained pipe: no single catalog function is two pipes, and translating half of
one would change the value without saying so.
### Validation
Every A2UI input is `Checkable`, which is the same job prefab's `required` and
`inputType` do, so a form keeps its validation on the way across:
```json
{
"component": "TextField",
"label": "Email",
"checks": [
{ "condition": { "call": "required", "args": { "value": { "path": "/email" } } } },
{ "condition": { "call": "email", "args": { "value": { "path": "/email" } } } }
]
}
```
No `message` is emitted. The rule already says which check failed, and the
renderer is better placed to word and localize that than prefab is.
`numeric` is deliberately not emitted for a number field. The catalog requires it
to carry a `min` or a `max` — it is a range check rather than a type check — and
prefab's number inputs carry no range. `variant: 'number'` on the field already
says the value is numeric.
### Control flow
| prefab | A2UI |
|---|---|
| `ForEach` | the child template — one instance per item, `$item` resolving to a path relative to the current item and `$index` to the `@index` function |
| `Define` / `Use` / `Slot` | resolved at emit time by inlining the definition; a `Use`'s `overrides` are seeded into the data model and brought into scope by name |
| `If` / `Elif` / `Else` / `Condition` | **no equivalent** |
Conditionals are the one real capability gap between the two protocols. A2UI has
no declarative `if`: the renderer draws what the adjacency list says, and the
agent sends a fresh `updateComponents` when the shape should change. prefab runs
a reactive client that re-shapes itself without a round trip.
A one-item list template would *look* like a conditional and behave like one only
by accident, so the emitter reports the loss instead of faking it. A UI leaning
on `If` does not cross over intact, and no amount of emitter work changes that.
### Tables
`DataTable` and `autoTable` map onto A2UI's child template rather than being
flattened row by row: one `Row` template, one `Text` per column bound to the
column key, and a `Column` whose `children` is `{ path, componentId }`. The
renderer instantiates one copy per item in the data-model list, so the emitted
surface stays as small and as reactive as the prefab original.
A literal row array is seeded into the data model so the template has something
to iterate; a `{{ rows }}` expression binds straight to where the rows already
live.
### Mapping table
| prefab | A2UI Basic | Note |
|---|---|---|
| `Column`, `Row` | `Column`, `Row` | `align` and `justify` carried across |
| `Div`, `Container`, `Grid`, `Form`, `Page`, … | `Column` | containers with no A2UI meaning flatten |
| `Card`, `CardContent` | `Card` | several children get a `Column` wrapper |
| `H1`–`H6`, `Heading` | `Text` | Markdown `#` prefix |
| `Text`, `P`, `Lead`, `Large`, `Markdown` | `Text` | |
| `Muted`, `Small`, `Label`, `Badge` | `Text` | `variant: caption` |
| `Code`, `Kbd` | `Text` | backtick-wrapped |
| `BlockQuote` | `Text` | `>` prefix |
| `Input`, `Textarea` | `TextField` | `inputType` picks the variant; `required` becomes a check |
| `Checkbox`, `Switch` | `CheckBox` | |
| `Select`, `RadioGroup`, `Combobox` | `ChoicePicker` | options read from the children |
| `Slider` | `Slider` | `step` converted to division count |
| `DatePicker`, `TimePicker` | `DateTimeInput` | |
| `Button` | `Button` | label becomes a child `Text` |
| `Link` | `Button` | borderless, running the `openUrl` function |
| `Image`, `Video`, `Audio`, `Icon` | `Image`, `Video`, `AudioPlayer`, `Icon` | |
| `Tabs` / `Tab` | `Tabs` | |
| `Dialog` | `Modal` | |
| `Separator` | `Divider` | |
| `Alert` | `Card` | variant styling dropped |
| `Metric` | `Column` of `Text` | trend and delta dropped |
| `Table`, `DataTable` | `Column` of `Row`s | see above |
| `CardTitle`, `CardDescription`, `Tooltip` | `Text` | |
| `ForEach` | templated `Column` | see Control flow |
| `Define`, `Use`, `Slot` | inlined | see Control flow |
| `If`, `Elif`, `Else`, `Condition` | — | `unsupported` |
| charts, `Mermaid`, `Svg`, `DropZone`, `Progress` | — | `unsupported` |
A component the table does not name still emits: one with children flattens to a
`Column`, one with text renders as `Text`, and each raises a `degraded`
diagnostic. Nothing is dropped silently.
### Actions
| prefab action | A2UI |
|---|---|
| `CallTool` (either `toolCall` or `callTool` on the wire) | `{ event: { name: tool, context: arguments } }` |
| `SendMessage` | `{ event: { name: 'sendMessage', context: { message } } }` |
| `OpenLink` | `{ functionCall: { call: 'openUrl', args: { url } } }` |
| `SetState`, `ToggleState`, everything else | an agent event named after the action |
Argument values go through the same binding conversion as component props, so
`CallTool('search', { arguments: { q: '{{ query }}' } })` sends the bound value
rather than the raw template.
A2UI carries one action per control. Where prefab binds several, the first is
used and the rest raise an `action` diagnostic.
## Conformance
Emitted payloads are validated against the official A2UI v1.0 JSON Schemas,
vendored under `test/fixtures/a2ui/v1_0/`, plus the two structural rules the
schemas cannot express: every referenced child id must exist, and every
component must be reachable from `root`. `test/a2ui.test.ts` runs one view per
mapper family through that gate.
Refresh the vendored schemas when A2UI publishes a revision:
```bash
bun scripts/sync-a2ui-schemas.ts
```
---
---
url: /prefab/guide/renderer.md
description: >-
Mount $prefab wire JSON into the browser with the vanilla DOM renderer. Zero
dependencies, works in any iframe or web page.
---
# Browser Renderer
The browser renderer is how your `$prefab` UIs come alive in a real page. It is a single, self-contained script (`dist/renderer.min.js`) with **zero framework dependencies** — no React, no Vue, no build step. Drop it in with a `
```
**Manual mount** is for when the data arrives later — from a fetch, a tool call, or user interaction. Call `PrefabRenderer.mount(element, data, options)` yourself and you get back a **render handle** you can hold onto: feed it new data with `update()`, force a redraw with `rerender()`, reach into the reactive `store`, or tear everything down with `destroy()`.
::: tip Always ship prefab.css
The renderer paints structure, but `prefab.css` carries the design tokens and base styles. Load it alongside the script or your components come out unstyled.
:::
## Going further
The renderer is extensible without forking it. You can teach it new node types with `registerComponent`, add new `{{ }}` filters with `registerPipe`, and inject scoped stylesheets straight from the wire format — all covered in the reference.
→ See the [Renderer API reference](/reference/api/renderer/) for the full API: every method, option, and the render handle.
---
---
url: /prefab/guide/bridge.md
description: >-
PostMessage bridge connecting prefab iframes to MCP Apps hosts (VS Code,
Claude, ChatGPT) via prefab:* and ui/* JSON-RPC protocols.
---
# PostMessage Bridge
When a prefab app renders inside an MCP Apps host like VS Code, Claude, or ChatGPT, it lives in a sandboxed iframe. The bridge is what lets that iframe and its host hold a conversation — passing tool input in, sending tool calls and chat messages out, and reacting to theme or context changes — all over the browser's `postMessage` channel.
## Two protocols, one bridge
The bridge speaks two message dialects at once, so the same app works across hosts that disagree on conventions:
* **`prefab:*`** — the custom, prefab-native protocol (for example `prefab:tool-input`, `prefab:tool-call`).
* **`ui/*`** — the JSON-RPC dialect from the [MCP Apps spec](https://modelcontextprotocol.io), which standards-based hosts prefer.
You never choose between them. The bridge negotiates the right one during connection and translates underneath, so your code stays the same regardless of which host loaded it.
## Connection lifecycle
Every session opens with a short handshake. The app announces the capabilities it supports, the host replies with its own capabilities plus the active theme, and from there messages flow both ways for the life of the iframe. Tool input arrives, your app renders, tool calls go back out, and either side can tear the connection down cleanly when it's done. The host can also push updates mid-session — a theme switch or a fresh context payload (locale, access tokens, and the like) — which your handlers receive as they happen.
## High-level vs. low-level
Most apps only ever touch the high-level **`prefab.app()`** factory. It auto-detects whether you're in an iframe or running standalone, runs the handshake for you, applies the host theme, and hands back a ready-to-use `PrefabApp` object with friendly methods like `onToolInput`, `render`, and `callTool`.
```html
```
If you need finer control — a custom transport, your own handshake timing, or raw message handling — reach for the low-level **`Bridge`** class instead. It exposes the wire directly: connect, initialize, subscribe to raw message types, and disconnect yourself. The factory is built on top of it, so you lose convenience but gain control.
## A note on origin security
By default the bridge accepts messages from any origin (`'*'`), which is fine for local development but unsafe in production. Always pass an explicit `hostOrigin` so the bridge only trusts messages from your real host. It validates incoming `event.origin`, correlates tool responses by id to prevent spoofing, and times out stalled tool calls — but the origin you set is the first line of defense.
→ See the [Renderer API reference](/reference/api/renderer/) for the full API: the `Bridge` class, the `PrefabApp` methods, and the `app()` factory.
---
---
url: /prefab/reference/components.md
description: >-
Complete API reference for all 115+ prefab components — props, signatures, and
wire format examples for layout, forms, data, media, and charts.
---
# Components Reference
All components are functions that return a `Component` instance. They serialize to JSON via `.toJSON()` and compose as children of container components.
***
## Layout
Structural containers that control spacing, direction, and grid placement.
### `Column(props?)`
Vertical flex container. The most common layout primitive.
```ts
Column({ gap: 6, children: [H1('Title'), Text('Body')] })
Column({ children: [Text('No gap')] }) // shorthand
```
| Prop | Type | Description |
|------|------|-------------|
| `gap` | `number \| GapToken` | Spacing between children. Accepts a number or semantic token: `'none'`, `'xs'`, `'sm'`, `'md'`, `'lg'`, `'xl'`, `'2xl'` |
| `align` | `string` | Cross-axis alignment (`start`, `center`, `end`, `stretch`) |
| `justify` | `string` | Main-axis alignment |
| `cssClass` | `RxStr` | Extra CSS class — supports reactive expressions (e.g. `rx('active').then('bg-green', 'bg-red')`) |
| `onClick` | `Action \| Action[]` | Action(s) dispatched on click. Non-button elements get `role="button"` and keyboard support automatically |
**Gap tokens** map to numbers: `none`=0, `xs`=1, `sm`=2, `md`=3, `lg`=4, `xl`=6, `2xl`=8.
```ts
Column({ gap: 'md', children: [Text('Hello')] }) // same as gap: 3
Row({ gap: 'xl', children: [Button('A'), Button('B')] })
```
### `Row(props?)`
Horizontal flex container.
```ts
Row({ gap: 4, children: [Button('Cancel'), Button('Save')] })
```
Same props as `Column`.
### `Grid(props?)`
CSS Grid container.
```ts
Grid({ columns: 3, gap: 4, children: [
GridItem({ colSpan: 2, children: [Card({ children: [Text('Wide')] })] }),
GridItem({ children: [Card({ children: [Text('Narrow')] })] }),
] })
```
| Prop | Type | Description |
|------|------|-------------|
| `columns` | `number` | Number of columns |
| `gap` | `number \| GapToken` | Grid gap (number or semantic token) |
### `GridItem(props?)`
Child of `Grid`.
| Prop | Type | Description |
|------|------|-------------|
| `colSpan` | `number` | Column span |
| `rowSpan` | `number` | Row span |
### `Container(props?)`
Generic wrapper with max-width and padding.
### `Div(props?)` / `Span(props?)`
Generic block/inline wrappers.
### `Dashboard(props?)` / `DashboardItem(props?)`
Dashboard grid layout with named items.
### `Pages(props?)` / `Page(props?)`
Paginated view container.
### `Detail(props)`
Conditional detail pane. Shows `children` when `of` resolves, shows `empty` otherwise.
```ts
import { collection, signal, Detail, Heading, Text } from '@maxhealth.tech/prefab'
const patients = collection('patients', data, { key: 'id' })
const selectedId = signal('selectedPatientId', patients.firstKey())
const selected = patients.by(selectedId)
Detail({ of: selected, empty: Text('Select a patient'), children: [
Heading(selected.dot('name')),
Text(selected.dot('dob')),
] })
```
| Prop | Type | Description |
|------|------|-------------|
| `of` | `Ref \| RxStr` | Reactive reference expression |
| `empty` | `Component` | Shown when ref is null/undefined |
### `MasterDetail(props?)`
Two-pane layout (master list + detail). Expects two children.
```ts
MasterDetail({ masterWidth: '350px', gap: 4, children: [
table, // master panel
detail, // detail panel
] })
```
| Prop | Type | Description |
|------|------|-------------|
| `masterWidth` | `string` | Master pane width (default: `'33%'`) |
| `gap` | `number` | Gap between panes |
***
## Typography
Text rendering components.
### `Heading(content, props?)`
```ts
Heading('Welcome', { level: 2 })
```
| Prop | Type | Description |
|------|------|-------------|
| `content` | `string \| Rx` | Text content (supports reactive expressions) |
| `level` | `1-4` | Heading level |
### `H1(content)` / `H2(content)` / `H3(content)` / `H4(content)`
Shorthand heading constructors.
```ts
H1('Dashboard') // same as Heading('Dashboard', { level: 1 })
```
### `Text(content, props?)`
```ts
Text('Hello world')
Text('Welcome, {{ userName }}!')
```
### `P(content)` / `Lead(content)` / `Large(content)` / `Small(content)` / `Muted(content)`
Semantic text variants.
### `BlockQuote(content)`
Block quotation.
### `Label(content, props?)`
Form label text.
### `Link(content, props?)`
```ts
Link('Visit site', { href: 'https://example.com', target: '_blank' })
```
| Prop | Type | Description |
|------|------|-------------|
| `href` | `string` | URL |
| `target` | `string` | Link target (`_blank`, `_self`, etc.) |
### `Code(content)` / `Kbd(content)`
Inline code / keyboard shortcut styling.
### `Markdown(content)`
Rendered Markdown content.
```ts
Markdown('## Hello\n\nThis is **bold** text.')
```
***
## Card
Card containers for grouped content.
### `Card(props?)`
```ts
Card({ children: [
CardHeader({ children: [CardTitle('User'), CardDescription('Profile info')] }),
CardContent({ children: [Text('Name: Alice')] }),
CardFooter({ children: [Button('Edit')] }),
] })
// With variant:
Card({ variant: 'elevated', children: [CardContent({ children: [Text('Raised card')] })] })
```
| Prop | Type | Description |
|------|------|-------------|
| `variant` | `CardVariant` | `'default'` | `'outline'` | `'ghost'` | `'elevated'` | `'destructive'` |
### `CardHeader` / `CardTitle` / `CardDescription` / `CardContent` / `CardFooter`
Card sub-components. All accept children.
***
## Data Display
### `DataTable(props)`
Rich data table with search, column definitions, and optional row selection.
```ts
DataTable({
rows: users,
columns: [
col('name', 'Name'),
col('email', 'Email'),
col('status', 'Status'),
],
search: true,
})
```
| Prop | Type | Description |
|------|------|-------------|
| `rows` | `unknown[] \| RxStr` | Array of row objects (or reactive expression) |
| `columns` | `DataTableColumnDef[]` | Column definitions (use `col()`) |
| `search` | `boolean` | Enable search |
| `from` | `Collection` | Derive rows from a Collection (mutually exclusive with `rows`) |
| `selected` | `Signal` | Signal tracking selected row key (requires `from`) |
#### Row Selection with Signal/Collection
When `from` and `selected` are provided, DataTable auto-generates row click handling:
```ts
const patients = collection('patients', data, { key: 'id' })
const selectedId = signal('selectedPatientId', patients.firstKey())
DataTable({
columns: [col('name'), col('dob')],
from: patients,
selected: selectedId,
})
// Wire: rows="{{ patients }}", rowKey="id", selected="{{ selectedPatientId }}",
// onRowClick=[{ action: "setState", key: "selectedPatientId", value: "{{ $item.id }}" }]
```
### `col(key, header?, opts?)`
Column definition helper — short form or descriptor form.
```ts
// Short form
col('name', 'Full Name')
col('email', 'Email', { sortable: true })
// Descriptor form (object)
col({ key: 'amount', header: 'Amount', format: 'currency' })
col({ key: 'name', accessor: 'name | humanName', header: 'Patient' })
```
| Field | Type | Description |
|-------|------|-------------|
| `key` | `string` | Row object field name |
| `header` | `string` | Column header (defaults to `key`) |
| `sortable` | `boolean` | Enable column sorting |
| `format` | `string` | Pipe name for cell display (e.g. `'currency'`) |
| `accessor` | `string` | Pipe expression for complex access |
### `Badge(content, props?)`
```ts
Badge('Active', { variant: 'success' })
```
| Variant | Color |
|---------|-------|
| `default` | Neutral |
| `secondary` | Muted |
| `outline` | Border only |
| `success` | Green |
| `warning` | Yellow |
| `destructive` | Red |
| `info` | Blue |
### `Metric(props)`
```ts
Metric({ label: 'Revenue', value: '$125K', delta: '+12.5%' })
```
| Prop | Type | Description |
|------|------|-------------|
| `label` | `string` | Metric name |
| `value` | `string \| number` | Display value |
| `change` | `number` | Percentage change (positive = green, negative = red) |
| `prefix` | `string` | Value prefix (e.g. `$`) |
| `suffix` | `string` | Value suffix (e.g. `%`) |
### Other Data Components
| Component | Description |
|-----------|-------------|
| `Dot(props)` | Colored status dot |
| `Ring(props)` | Circular progress ring |
| `Progress(props)` | Linear progress bar |
| `Separator()` | Horizontal divider |
| `Loader()` | Loading spinner |
| `Icon(name, props?)` | Named icon |
***
## Table
Low-level HTML table primitives (for custom table layouts beyond `DataTable`).
```ts
Table({ striped: true, children: [
TableHead({ children: [
TableRow({ children: [TableHeader('Name'), TableHeader('Age')] }),
] }),
TableBody({ children: [
TableRow({ children: [TableCell({ children: [Text('Alice')] }), TableCell({ children: [Text('30')] })] }),
] }),
TableCaption('User list'),
] })
```
### Components
`Table`, `TableHead`, `TableBody`, `TableFooter`, `TableRow`, `TableHeader`, `TableCell`, `TableCaption`, `ExpandableRow`
`TableCell` supports `colSpan` and `rowSpan` props.
***
## Form
### `Form(props?)`
```ts
Form({ onSubmit: new CallTool('create_user'), children: [
Input({ name: 'email', inputType: 'email', required: true }),
Button('Create', { submit: true }),
] })
```
| Prop | Type | Description |
|------|------|-------------|
| `onSubmit` | `Action` | Action to run when the form is submitted |
### `Input(props)`
```ts
Input({ name: 'email', inputType: 'email', label: 'Email', placeholder: 'you@example.com', required: true })
```
| Prop | Type | Description |
|------|------|-------------|
| `name` | `string` | State key (also used as form field name) |
| `type` | `string` | `text`, `email`, `number`, `password`, `url`, `tel`, `date`, `search`, `hidden` |
| `label` | `string` | Label text |
| `placeholder` | `string` | Placeholder text |
| `required` | `boolean` | Validation |
| `defaultValue` | `string` | Initial value |
| `onChange` | `Action` | Action on value change |
### `Textarea(props)`
Multi-line text input. Same props as `Input` plus `rows`.
### `Button(content, props?)`
```ts
Button('Save', { variant: 'default', onClick: new ShowToast('Saved!') })
```
| Prop | Type | Description |
|------|------|-------------|
| `variant` | `ButtonVariant` | `default`, `secondary`, `outline`, `ghost`, `destructive`, `link` |
| `size` | `ButtonSize` | `default`, `sm`, `lg`, `icon` |
| `onClick` | `Action` | Click action |
| `type` | `string` | `button` (default), `submit` |
| `disabled` | `boolean` | Disabled state |
### `ButtonGroup(children)`
Horizontal button row.
### `Select(props?)`
```ts
Select({ name: 'role', label: 'Role', required: true, children: [
SelectOption('admin', 'Admin'),
SelectOption('user', 'User'),
] })
```
| Prop | Type | Description |
|------|------|-------------|
| `name` | `string` | State key the choice is stored under |
| `label` | `string` | Visible field label |
| `value` | `string \| string[]` | Pre-selected choice, or choices when `multiple` |
| `placeholder` | `string` | Shown until a choice is made |
| `required` | `boolean` | Must be filled before submit |
| `multiple` | `boolean` | Accept several choices; submits an array under `name` |
| `onChange` | `Action` | Fired with the chosen value |
Sub-components: `SelectOption`, `SelectGroup`, `SelectLabel`, `SelectSeparator`
`required` is shared by every stateful control, so `RadioGroup`, `Combobox`,
`Textarea`, `Calendar` and `DatePicker` take it the same way.
### `Checkbox(props)` / `Switch(props)` / `Slider(props)`
Boolean and range inputs.
```ts
Checkbox({ name: 'agree', label: 'I agree to the terms' })
Switch({ name: 'notifications', label: 'Enable notifications' })
Slider({ name: 'volume', min: 0, max: 100, step: 1 })
```
### `Radio(props)` / `RadioGroup(props?)`
```ts
RadioGroup({ name: 'color', label: 'Favorite Color', children: [
Radio({ value: 'red', label: 'Red' }),
Radio({ value: 'blue', label: 'Blue' }),
Radio({ value: 'green', label: 'Green' }),
] })
```
### `Combobox(props?)` / `ComboboxOption(props)`
Autocomplete select with search.
```ts
Combobox({ name: 'country', placeholder: 'Search countries...', searchable: true, children: [
ComboboxOption('us', 'United States'),
ComboboxOption('de', 'Germany'),
] })
```
Sub-components: `ComboboxGroup`, `ComboboxLabel`, `ComboboxSeparator`
### `Calendar(props)` / `DatePicker(props)`
Date selection.
```ts
Calendar({ name: 'date', minDate: '2024-01-01', maxDate: '2024-12-31' })
DatePicker({ name: 'dob', label: 'Date of Birth', placeholder: 'Pick a date' })
```
### `Field(props?)`
Structured form field wrapper.
```ts
Field({ children: [
FieldTitle('Email'),
FieldDescription('Your work email address'),
FieldContent({ children: [Input({ name: 'email', inputType: 'email' })] }),
FieldError('Invalid email format'),
] })
```
Sub-components: `FieldTitle`, `FieldDescription`, `FieldContent`, `FieldError`
### `ChoiceCard(props)`
Selectable card for option picking.
```ts
ChoiceCard({ value: 'pro', label: 'Pro Plan', description: '$29/mo', selected: true })
```
***
## Interactive
### `Tabs(props?)`
Tabbed interface with keyboard navigation (Arrow keys, Home/End).
```ts
Tabs({ defaultTab: 'overview', children: [
Tab({ id: 'overview', title: 'Overview', children: [Text('Overview content')] }),
Tab({ id: 'details', title: 'Details', children: [Text('Details content')] }),
] })
```
### `Accordion(props?)`
Collapsible sections.
```ts
Accordion({ children: [
AccordionItem({ title: 'FAQ 1', children: [Text('Answer 1')] }),
AccordionItem({ title: 'FAQ 2', children: [Text('Answer 2')] }),
] })
```
### `Dialog(props?)`
Modal dialog (ARIA `role="dialog"`).
```ts
Dialog({ title: 'Confirm', trigger: Button('Delete'), children: [
Text('Are you sure?'),
Button('Delete', { variant: 'destructive', onClick: new CallTool('delete_item') }),
] })
```
### `Popover(props?)` / `Tooltip(props?)` / `HoverCard(props?)`
Overlay components.
### `Carousel(props?)`
Image/content carousel with prev/next buttons.
***
## Charts
All charts accept a `data` prop (array of objects) and a `series` array.
### `BarChart(props)` / `LineChart(props)` / `AreaChart(props)` / `PieChart(props)`
```ts
BarChart({
data: [{ month: 'Jan', revenue: 100 }, { month: 'Feb', revenue: 150 }],
series: [{ dataKey: 'revenue', label: 'Revenue', color: '#3b82f6' }],
xAxis: 'month',
valueFormat: 'currency',
height: 300,
})
```
| Prop | Type | Description |
|------|------|-------------|
| `data` | `object[]` | Data array |
| `series` | `ChartSeries[]` | Series definitions (see below) |
| `xAxis` | `string` | Data key for X axis labels |
| `xAxisFormat` | `string` | Pipe applied to X axis tick labels (e.g. `'date'`, `'truncate:10'`) |
| `tooltipXKey` | `string` | Data key for tooltip category label (defaults to `xAxis`) |
| `tooltipXFormat` | `string` | Pipe applied to tooltip category label (e.g. `'datetime'`, `'upper'`) |
| `height` | `number` | Chart height in px |
| `showTooltip` | `boolean` | Show tooltip on hover (default `true`) |
| `showGrid` | `boolean` | Show horizontal grid lines |
| `showYAxis` | `boolean` | Show Y axis labels (default `true`) |
| `valueFormat` | `string` | **Canonical** value-axis tick + tooltip format: `'currency'`, `'percent:1'`, `'compact'`, … (`'auto'` = none). Matches upstream prefab (PR #454). |
| `yAxisFormat` | `string` | Left-axis override for dual-axis charts (overrides `valueFormat`) |
| `showYAxisRight` | `boolean` | Show secondary Y axis on the right |
| `yAxisRightFormat` | `string` | Format for the right Y axis |
| `showLegend` | `boolean` | Show legend below chart |
`PieChart` takes `dataKey` (numeric value) + `nameKey` (slice label) instead of `series`/`xAxis` (the legacy series form is still accepted), plus `innerRadius`, `showLabel`, `paddingAngle`, and `valueFormat`.
#### `ChartSeries`
| Field | Type | Description |
|-------|------|-------------|
| `dataKey` | `string` | **Required.** Key in data objects for this series |
| `label` | `string` | Display label (defaults to `dataKey`) |
| `color` | `string` | Series color (auto-assigned if omitted) |
| `yAxisId` | `'left' \| 'right'` | Which Y axis this series binds to |
| `tooltipFormat` | `string` | Pipe for this series' value in the tooltip (overrides `yAxisFormat`) |
#### Formatting Example
Same timestamp field, different presentation on axis vs tooltip:
```ts
LineChart({
data: timeseries,
xAxis: 'timestamp',
xAxisFormat: 'date', // axis: "4/25/2026"
tooltipXFormat: 'datetime', // tooltip: "4/25/2026, 2:30:00 PM"
series: [
{ dataKey: 'revenue', label: 'Revenue', tooltipFormat: 'currency' },
{ dataKey: 'growth', label: 'Growth', tooltipFormat: 'percent' },
],
})
```
All built-in pipes work: `upper`, `lower`, `truncate`, `currency`, `percent`, `compact`, `date`, `time`, `datetime`, `number`, `round`, plus custom wire pipes.
### `RadarChart(props)`
Spider/radar chart — one polygon per series across angular axes.
```ts
RadarChart({
data: [{ subject: 'Math', alice: 120 }, { subject: 'English', alice: 98 }],
series: [{ dataKey: 'alice', label: 'Alice' }],
axisKey: 'subject', // spoke labels (falls back to xAxis)
filled: true, // fill polygons (default true)
showDots: false, // vertices
})
```
### `ScatterChart(props)`
Scatter / bubble chart.
```ts
ScatterChart({
data: [{ h: 170, w: 65, age: 25 }, { h: 180, w: 80, age: 30 }],
series: [{ dataKey: 'people', label: 'People' }],
xAxis: 'h',
yAxis: 'w',
zAxis: 'age', // optional — sizes each point (bubble chart)
})
```
### `Sparkline(props)`
Inline mini chart.
```ts
Sparkline({ data: [10, 20, 15, 30, 25], variant: '#22c55e', height: 32 })
```
### `RadialChart(props)` / `Histogram(props)`
Radial bar chart (concentric value arcs) and histogram distribution chart.
```ts
RadialChart({
data: [{ browser: 'Chrome', visitors: 275 }, { browser: 'Safari', visitors: 200 }],
dataKey: 'visitors', // value (legacy series form still accepted)
nameKey: 'browser', // label
innerRadius: 30, startAngle: 180, endAngle: 0,
})
```
> All chart types now render natively as SVG in the built-in renderer.
***
## Media
### `Image(src, opts?)` / `Image(props)`
Positional form (consistent with Audio/Video/Embed) or props form:
```ts
Image('/photo.jpg', { alt: 'Profile photo' }) // positional (recommended)
Image({ src: '/photo.jpg', alt: 'Profile photo' }) // props form (also works)
```
### `Audio(props)` / `Video(props)` / `Embed(props)`
Media embeds.
### `Svg(props)`
Inline SVG content.
### `DropZone(props)`
File upload drop area. Accepts files by drag-and-drop or by click-to-browse, and is keyboard reachable.
```ts
DropZone({
accept: 'image/*',
multiple: true,
resultKey: 'uploads',
onDrop: new CallTool('upload_file'),
})
```
| Prop | Type | Description |
|------|------|-------------|
| `label` | `RxStr` | Prompt shown inside the area. Defaults to `Drop files here` |
| `accept` | `string` | Accepted types, as the HTML `accept` attribute (`image/*`, `.pdf`, `text/csv`). Enforced for dropped files too, which the browser does not do |
| `multiple` | `boolean` | Allow more than one file. When false, only the first accepted file is taken |
| `resultKey` | `string` | State key the chosen files are written to |
| `onDrop` | `Action \| Action[]` | Fired once files are chosen, with `$result` bound to the file list |
`resultKey` and `$result` match the `OpenFilePicker` action, so both paths to files behave the same.
### `Mermaid(content)`
Mermaid diagram (rendered by the browser if `mermaid` library is available).
```ts
Mermaid('graph TD; A-->B; B-->C;')
```
***
## Alert
### `Alert(props?)`
```ts
Alert({ variant: 'warning', children: [
AlertTitle('Warning'),
AlertDescription('This action cannot be undone.'),
] })
Alert({ variant: 'success', icon: 'CheckCircle', children: [
AlertTitle('Saved'),
AlertDescription('Changes applied successfully.'),
] })
```
| Variant | Color |
|---------|-------|
| `default` | Neutral |
| `success` | Green |
| `warning` | Yellow |
| `destructive` | Red |
| Prop | Type | Description |
|------|------|-------------|
| `variant` | `AlertVariant` | `'default'` | `'destructive'` | `'success'` | `'warning'` |
| `icon` | `string` | Icon name (e.g. `'CheckCircle'`, `'AlertTriangle'`) |
***
## Control Flow
### `ForEach(props?)`
Iterate over a reactive array.
```ts
ForEach({ expression: rx('items'), children: [
Text(ITEM.dot('name')),
] })
```
| Prop | Type | Description |
|------|------|-------------|
| `each` | `Rx \| string` | Expression for the array to iterate |
| `as` | `string` | Variable name for each item (default: `item`) |
### `If(condition, children)` / `If(props)` / `Elif` / `Else`
Conditional rendering. Supports both shorthand and props form:
```ts
// Shorthand (recommended):
If('$loading', [Loader()])
Elif('$error', [Alert({ variant: 'destructive', children: [Text('$error')] })])
Else({ children: [Text('Content loaded!')] })
// Props form (also works):
If({ condition: '$loading', children: [Loader()] })
```
### `Define(props?)` / `Use(props)` / `Slot(props?)`
Component templates for reuse.
```ts
Define({ name: 'userCard', children: [
Card({ children: [CardContent({ children: [
Slot({ name: 'name' }),
Slot({ name: 'role' }),
] })] }),
] })
// Later:
Use({ def: 'userCard', overrides: { name: 'Alice', role: 'Admin' } })
```
---
---
url: /prefab/reference/wire-format.md
description: >-
$prefab v0.3 wire format specification — JSON structure, component nodes,
state, actions, pipes, defs, and template slots.
---
# 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": { ... }
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `$prefab` | `{ version: string }` | **Yes** | Format identifier. Current version: `"0.3"` |
| `view` | `ComponentJSON` | **Yes** | Root component tree |
| `state` | `Record` | No | Initial reactive state |
| `css` | `string[]` | No | Inline CSS blocks injected as `