Prototyper UI

Code Export

Convert specs to copy-paste React/JSX code

The specToJSX() function converts any Stream UI spec into a self-contained React component file. This lets users go from AI-generated UI to real, editable code in one step.

Basic Usage

import { specToJSX } from "@prototyperai/stream-ui"

const spec = {
  root: "card",
  elements: {
    card: { type: "Card", props: {}, children: ["heading", "btn"] },
    heading: { type: "Heading", props: { text: "Hello World", level: 2 } },
    btn: { type: "Button", props: { label: "Click me" } },
  },
}

const code = specToJSX(spec)

Output:

"use client"

import { Card } from "@/registry/ui/card"
import { Button } from "@/registry/ui/button"

export function GeneratedUI() {
  return (
    <Card>
      <h2>
        Hello World
      </h2>
      <Button>
        Click me
      </Button>
    </Card>
  )
}

Options

Pass a CodegenOptions object to customize the output:

const code = specToJSX(spec, {
  indent: 4,
  componentName: "LoginForm",
  importPrefix: "@/components/ui",
})

Prop

Type

How Components Map to Imports

Each component type maps to an import file. For example, Button imports from {prefix}/button, CardTitle imports from {prefix}/card, and SelectItem imports from {prefix}/select. Multiple components from the same file are grouped into a single import statement.

Two component types map to HTML elements instead of imports:

  • Heading renders as <h1> through <h6> based on the level prop
  • Text renders as <p>

Dynamic Value Conversion

State Expressions

$state expressions convert to variable references using camelCase naming derived from the JSON Pointer path:

{ "text": { "$state": "/user/name" } }
{userName}

Two-Way Bindings

$bindState expressions generate value + onChange pairs appropriate to the component type:

ComponentGenerated Code
Input, Textareavalue={email} onChange={(e) => setEmail(e.target.value)}
Checkbox, Switchchecked={agreed} onCheckedChange={setAgreed}
Select, RadioGroupvalue={role} onValueChange={setRole}
Slidervalue={[volume]} onValueChange={([v]) => setVolume(v)}

Template Expressions

$template strings convert to template literals:

{ "content": { "$template": "Hello ${/name}!" } }
{`Hello ${name}!`}

Computed and Conditional Expressions

$computed and $cond expressions generate placeholder comments since their logic cannot be statically resolved:

{/* computed: formatDate */}
{/* conditional value */}

Item and Index Expressions

Inside repeated elements, $item and $index expressions reference the .map() callback parameters:

{item.title}
{index}

State Management

When the spec includes a state object, specToJSX generates useState hooks for each top-level state key:

{ "state": { "count": 0, "items": [], "name": "World" } }
import { useState } from "react"

export function GeneratedUI() {
  const [count, setCount] = useState(0)
  const [items, setItems] = useState([])
  const [name, setName] = useState("World")

  // ...
}

Actions

Event bindings in the on field convert to inline handlers:

ActionGenerated Code
setStatesetCount(5) or setName(otherVar)
toggleStatesetActive((prev) => !prev)
appendItemsetItems((prev) => [...prev, newItem])
removeItemsetItems((prev) => prev.filter((_, i) => i !== index))
Custom actionsComment with action name and params

The press event maps to onClick in React.

Repeat (Lists)

Elements with a repeat binding generate .map() calls:

{
  "type": "Card",
  "repeat": { "source": "/todos", "itemKey": "id" },
  "children": ["todoItem"]
}
{todos.map((item) => (
  <Card key={item.id}>
    {/* children */}
  </Card>
))}

Visibility (Conditional Rendering)

Elements with visible conditions generate conditional expressions:

{
  "type": "Text",
  "props": { "content": "Loading..." },
  "visible": { "$state": "/isLoading" }
}
{isLoading && (
  <p>
    Loading...
  </p>
)}

Complex conditions use the appropriate operators:

{ "visible": { "$state": "/count", "gt": 0 } }
{count > 0 && (
  // ...
)}

Limitations

The code export is designed to produce a working starting point, not a perfect final result. Some dynamic behavior cannot be statically converted:

  • $computed expressions become comments since the function implementations live outside the spec
  • $cond expressions become placeholder comments
  • Custom actions (not setState/toggleState/appendItem/removeItem) generate comment stubs
  • Cross-field validation is not included in the exported code
  • State is flat — nested state paths are converted to flat useState calls at the top level

The intended workflow is: AI generates a spec, the user previews it live with the Renderer, and then exports to code for further customization.

Use Case

AI Model  -->  Streaming Spec  -->  Live Preview (Renderer)
                                        |
                                    [Export]
                                        |
                                   JSX Component  -->  Customize & Ship

The exported code uses your project's actual component imports, so it integrates directly into your codebase without any Stream UI runtime dependency.

On this page