01
Define components, functions
Your design system and your endpoints, each behind a schema: the closed set the model builds from.
02
AI generates
Derived values, visibility and events are short JavaScript expressions, checked before they run.
03
Get an app, logic included
Elements mount as they arrive. Clicks run callbacks that call your functions and write reactive state.
The model as your front-end developer
uicast is a UI framework whose language is written by models, not people. It gives a model what JSX gives a developer — components, state, events — but is built to be reliable to generate rather than easy to maintain. The model’s output is data: the engine turns it into a working front-end app, with no build or deploy step per page.
PromptShow this week's orders, with refund buttons.
Expressions hold the logic
What makes uicast different is expressions: computed values, visibility and event handling are JavaScript, run by a closed-grammar interpreter.
[
{
"key": "counter",
"component": "Counter",
"seed": [{ "set": "scopes.root.count", "literal": 0 }],
"props": { "expr": "({ count: scopes.root.count })" },
"callbacks": {
"onClick": [
{
"set": "scopes.root.count",
"expr": "currentValue + 1"
}
]
}
}
]How it's built— definition, implementation, prompt & wiring
import z from "zod";
import { createComponentDefinition } from "@uicast/core";
export const CounterDef = createComponentDefinition({
name: "Counter",
description: "A button that shows a number and increments it on each click.",
props: z.object({
count: z.number().default(0).meta({ description: "The number to display" }),
}),
callbacks: {
onClick: z.null().meta({ description: "Fires when the counter is pressed" }),
},
});import { createComponentImplementation } from "@uicast/react";
import { Button } from "@/components/ui/button";
import { CounterDef } from "./def";
export const CounterImpl = createComponentImplementation({
def: CounterDef,
render: ({ count, onClick }) => (
<Button variant="outline" onClick={() => onClick()}>
Count: {count}
</Button>
),
});# Available Components
Counter
## Component Details
- Counter — A button that shows a number and increments it on each click.
Props:
- count?: number = 0 — The number to display
Event handlers:
- onClick() — Fires when the counter is pressedimport { Evaluator } from "@uicast/expr";
import { EntriesRenderer, RendererProvider } from "@uicast/react";
import { CounterImpl } from "./impl";
import counterEntries from "./entries.json";
const implementations = [CounterImpl];
const evaluator = new Evaluator();
export function Counter() {
return (
<RendererProvider implementations={implementations} evaluator={evaluator}>
<EntriesRenderer entries={counterEntries} />
</RendererProvider>
);
}Structured generation offers a fixed set of bindings and operators instead, so a calculation outside that set needs a code change.
| Approach | Predictability | Expressiveness | Model code |
|---|---|---|---|
| Raw code generation | low | unlimited | any |
| uicast | high | high | expressions |
| Structured generation (json-render , A2UI ) | deterministic | limited | none |
The expression evaluator
Expressions run on an interpreter for a fraction of JavaScript: a
closed grammar, an allow-list of methods and globals, a check on every property
read, and a CPU and allocation budget. By default nothing reaches eval or
new Function, so a uicast app runs under a strict Content-Security-Policy,
without unsafe-eval. The interpreter secures the language, not what you plug
into it: host functions are capabilities you grant, and scope data is only as
safe as what you put there.
// scopes.root.orders === [{ total: 2 }, { total: 3 }]
scopes.root.orders.reduce((a, o) => a + o.total, 0) // ✅ → 5
listOrders() // ✅ a host function you registered,
// callable in seed and callbacks
fetch("/api/orders") // ❌ throws — "fetch" is not
// available in expressions
document.cookie // ❌ throws — so do "window",
// "localStorage", "eval", …The prompt is generated from your code
uicast prints the model’s list of components and functions from your component definitions and function schemas. Change a prop or a function, and the prompt changes with it.
# Available Components
Alert, Badge, BarChart, Button, Calendar, Card, Checkbox, DescriptionList, EmptyState, Field, FieldLabel, FlexCol, FlexRow, Form, Grid, Heading, Input, KanbanBoard, LineChart, LocationMap, Modal, NumberInput, Pagination, Picture, QRCode, Select, Slider, Stat, Switch, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, ToggleGroup, Typography
## Component Details
- Button — A clickable button.
Props:
- text?: string — The button label
- variant?: "default" | "destructive" | "outline" | …
Event handlers:
- onClick(evt: MouseEvent)
… one entry per component …
# Available Functions
listOrders, refundOrder, updateOrder, listProducts, placeOrder, listWarehouses, getSlots, bookSlot
## Function Details
- listOrders() => {
id: number /* integer */;
customer: string;
total: number;
status: "pending" | "paid" | "shipped" | "refunded";
}[]: Every order this week, newest first.
- refundOrder({
id: number /* integer */;
}) => unknown: Refund one order.
… one entry per function …Show this week’s orders, with refund buttons.
It is the first prompt in the demo at the top. Both lists are closed: the
model picks components from your catalog and calls only your
functions, each a standard-tool. The
descriptions are the ones you wrote on props and schema fields. See
Assembling the prompt.
Events carry declared payloads
An event can carry data. The square below sends the pointer position with
onMove; the entry writes it to state, and the props read it back. Inside a
callback, evt is the payload, shaped by the definition: here
evt.x and evt.y are numbers. It is not React’s SyntheticEvent.
[
{
"key": "pad",
"component": "TrackPad",
"seed": [{ "set": "scopes.root.pointer", "literal": {} }],
"props": {
"expr": "({ x: scopes.root.pointer.x, y: scopes.root.pointer.y })"
},
"callbacks": {
"onMove": [
{
"set": "scopes.root.pointer",
"expr": "({ x: evt.x, y: evt.y })"
}
],
"onLeave": [{ "set": "scopes.root.pointer", "literal": {} }]
}
}
]How it's built— definition, implementation, prompt & wiring
import z from "zod";
import { createComponentDefinition } from "@uicast/core";
export const TrackPadDef = createComponentDefinition({
name: "TrackPad",
description: "A square surface that reports the pointer position while hovered or pressed.",
props: z.object({
x: z.number().optional().meta({ description: "Pointer x, in px from the left edge" }),
y: z.number().optional().meta({ description: "Pointer y, in px from the top edge" }),
}),
callbacks: {
onMove: z
.object({
x: z.number().meta({ description: "Pointer x inside the square" }),
y: z.number().meta({ description: "Pointer y inside the square" }),
})
.meta({ description: "Fires on every pointer move over the square" }),
onLeave: z.null().meta({ description: "Fires when the pointer leaves" }),
},
});import { createComponentImplementation } from "@uicast/react";
import { Card, CardContent } from "@/components/ui/card";
import { TrackPadDef } from "./def";
export const TrackPadImpl = createComponentImplementation({
def: TrackPadDef,
render: ({ x, y, onMove, onLeave }) => (
<Card className="w-55">
<CardContent
className="relative grid h-40 cursor-crosshair touch-none place-items-center select-none"
onPointerMove={(e) => {
const rect = e.currentTarget.getBoundingClientRect();
onMove({
x: Math.round(e.clientX - rect.left),
y: Math.round(e.clientY - rect.top),
});
}}
onPointerLeave={() => onLeave()}
>
{x === undefined || y === undefined ? (
<span className="text-sm text-muted-foreground">Hover or tap here</span>
) : (
<>
<span className="font-mono text-sm">
x: {x} · y: {y}
</span>
<span
className="pointer-events-none absolute size-1.5 -translate-1/2 rounded-full bg-foreground"
style={{ left: x, top: y }}
/>
</>
)}
</CardContent>
</Card>
),
});# Available Components
TrackPad
## Component Details
- TrackPad — A square surface that reports the pointer position while hovered or pressed.
Props:
- x?: number — Pointer x, in px from the left edge
- y?: number — Pointer y, in px from the top edge
Event handlers:
- onMove(evt) — Fires on every pointer move over the square
- x: number — Pointer x inside the square
- y: number — Pointer y inside the square
- onLeave() — Fires when the pointer leavesimport { Evaluator } from "@uicast/expr";
import { EntriesRenderer, RendererProvider } from "@uicast/react";
import { TrackPadImpl } from "./impl";
import trackerEntries from "./entries.json";
const implementations = [TrackPadImpl];
const evaluator = new Evaluator();
export function Tracker() {
return (
<RendererProvider implementations={implementations} evaluator={evaluator}>
<EntriesRenderer entries={trackerEntries} />
</RendererProvider>
);
}Expressions call host functions
Expressions can call host functions: the functions you register, each a
standard-tool with a name, a description,
schemas and execute. They are how a document reaches your backend.
getWeather below is async (about 500 ms). Picking a city runs two steps: the
first stores the city, the second calls getWeather({ city: scopes.root.city }).
The second reads what the first wrote, so they run in order, and the result is
written to state when it arrives.
[
{
"key": "weather",
"component": "WeatherCard",
"seed": [
{ "set": "scopes.root.city", "literal": "Amsterdam" },
{
"set": "scopes.root.weather",
"expr": "getWeather({ city: scopes.root.city })"
}
],
"props": {
"expr": "({ city: scopes.root.city, tempC: scopes.root.weather?.tempC, condition: scopes.root.weather?.condition })"
},
"callbacks": {
"onCity": [
{ "set": "scopes.root.city", "expr": "evt.city" },
{
"set": "scopes.root.weather",
"expr": "getWeather({ city: scopes.root.city })"
}
]
}
}
]How it's built— definition, implementation, prompt & wiring
import z from "zod";
import { createComponentDefinition } from "@uicast/core";
export const WeatherCardDef = createComponentDefinition({
name: "WeatherCard",
description: "Shows the weather for a selectable city.",
props: z.object({
city: z.string().meta({ description: "The selected city" }),
tempC: z.number().optional().meta({ description: "Temperature, °C" }),
condition: z.string().optional().meta({ description: "Sky condition" }),
}),
callbacks: {
onCity: z
.object({ city: z.string().meta({ description: "The newly picked city" }) })
.meta({ description: "Fires when the user picks a city" }),
},
});import { createComponentImplementation } from "@uicast/react";
import { Card, CardContent } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { WeatherCardDef } from "./def";
const CITIES = ["Amsterdam", "Tokyo", "Oslo"];
export const WeatherCardImpl = createComponentImplementation({
def: WeatherCardDef,
render: ({ city, tempC, condition, onCity }) => (
<Card className="w-55">
<CardContent className="grid gap-3 text-center">
<Select value={city} onValueChange={(value) => onCity({ city: value })}>
<SelectTrigger size="sm" className="justify-self-center">
<SelectValue />
</SelectTrigger>
<SelectContent>
{CITIES.map((name) => (
<SelectItem key={name} value={name}>
{name}
</SelectItem>
))}
</SelectContent>
</Select>
{tempC === undefined ? (
<div className="text-sm text-muted-foreground">Loading…</div>
) : (
<>
<div className="text-3xl font-semibold">{tempC}°C</div>
<div className="text-sm text-muted-foreground">{condition}</div>
</>
)}
</CardContent>
</Card>
),
});import z from "zod";
import { standardTool } from "standard-tool";
const LATENCY_MS = 500;
const TABLE: Record<string, { tempC: number; condition: string }> = {
Amsterdam: { tempC: 14, condition: "Drizzle" },
Tokyo: { tempC: 24, condition: "Clear" },
Oslo: { tempC: 8, condition: "Cloudy" },
};
export const getWeather = standardTool({
name: "getWeather",
description: "Current weather for a city.",
inputSchema: z.object({
city: z.string().meta({ description: "City name" }),
}),
outputSchema: z.object({
tempC: z.number().meta({ description: "Temperature, °C" }),
condition: z.string().meta({ description: "One-word sky condition" }),
}),
async execute({ city }) {
await new Promise((resolve) => setTimeout(resolve, LATENCY_MS));
return TABLE[city] ?? { tempC: 20, condition: "Unknown" };
},
});# Available Components
WeatherCard
## Component Details
- WeatherCard — Shows the weather for a selectable city.
Props:
- city: string — The selected city
- tempC?: number — Temperature, °C
- condition?: string — Sky condition
Event handlers:
- onCity(evt) — Fires when the user picks a city
- city: string — The newly picked city
# Available Functions
getWeather
## Function Details
- getWeather({
city: string /* City name */;
}) => {
tempC: number /* Temperature, °C */;
condition: string /* One-word sky condition */;
}: Current weather for a city.import { Evaluator } from "@uicast/expr";
import { EntriesRenderer, RendererProvider } from "@uicast/react";
import { WeatherCardImpl } from "./impl";
import { getWeather } from "./tools";
import weatherEntries from "./entries.json";
const implementations = [WeatherCardImpl];
const evaluator = new Evaluator({ functions: [getWeather] });
export function Weather() {
return (
<RendererProvider implementations={implementations} evaluator={evaluator}>
<EntriesRenderer entries={weatherEntries} />
</RendererProvider>
);
}Chat replies carry live UI
The Streamdown plugin renders each uicast fenced block in a
Markdown reply as a live <EntriesRenderer>. Other fences
stay highlighted code:
Here are this week's orders:
```uicast
{"key":"orders","component":"Table", … }
```
Want me to refund any of them?The chat runs under one <RendererProvider>, so all blocks
share the root scope. Each block is still a complete document: lift one out
and it renders on its own, for example as a saved page.
Next steps
- Getting started — install and render your first document.
- Concepts — the model, expressions and scopes.
- Component definition and Component implementation — build your own components.
- Streaming — stream entries from a route and render them as they arrive.
