Skip to Content
Introduction

uicastbeta

The expression-driven generative UI framework

Sponsors

Starling MX

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.

A pipeline: your components and functions merge into one prompt, a model streams entries, and the entries render into a complete app built from those same components.

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.

0/25

PromptShow this week's orders, with refund buttons.

Model output
Rendered app

Expressions hold the logic

What makes uicast different is expressions: computed values, visibility and event handling are JavaScript, run by a closed-grammar interpreter.

Entriesthe LLM generates
[
  {
    "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"
        }
      ]
    }
  }
]
Result
How it's built— definition, implementation, prompt & wiring
Definitiondef.tsyou provide
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" }), }, });
Implementationimpl.tsxyou provide
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> ), });
Partial promptgenerated from definition
# 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 pressed
Rendererrenderer.tsxyou mount it
import { 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.

ApproachPredictabilityExpressivenessModel code
Raw code generationlowunlimitedany
uicasthighhighexpressions
Structured generation (json-render , A2UI )deterministiclimitednone

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.

Entriesthe LLM generates
[
  {
    "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": {} }]
    }
  }
]
Result
How it's built— definition, implementation, prompt & wiring
Definitiondef.tsyou provide
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" }), }, });
Implementationimpl.tsxyou provide
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> ), });
Partial promptgenerated from definition
# 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 leaves
Rendererrenderer.tsxyou mount it
import { 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.

Entriesthe LLM generates
[
  {
    "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 })"
        }
      ]
    }
  }
]
Result
How it's built— definition, implementation, prompt & wiring
Definitiondef.tsyou provide
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" }), }, });
Implementationimpl.tsxyou provide
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> ), });
Host functiontools.tsyou provide
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" }; }, });
Partial promptgenerated from definition
# 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.
Rendererrenderer.tsxyou mount it
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

Last updated on