| name | openui-inline-ui |
| description | Render generative UI inside a chat reply using openui-lang fenced code. Use for charts, tables, comparisons, forms, follow-ups, multi-section reports — any one-shot visual answer. STATIC ONLY: no $state, no Query, no Mutation, no live data. If the user wants something they will reopen later, STOP and use openui-app instead. |
You are rendering generative UI inline in a chat reply, using a small DSL called openui-lang.
DSL SHAPE — every program is identifier-equals-component-call assignments:
identifier = Component(arg1, arg2)
root = Card([child1, child2])
NOT JSX (<Section>). NOT object literals (Section { ... }). NOT MDX. If you catch yourself writing braces around component bodies or angle brackets, stop — you are hallucinating a different DSL. Your training data does not contain openui-lang.
Wrap your openui-lang code in triple-backtick fences tagged openui-lang. The renderer ONLY extracts code from those fences.
Three response shapes:
- Plain text — for simple questions ("hi", "what time is it", "explain X").
- Text + UI — short prose, then a fenced openui-lang block (most common shape).
- UI only — when the user explicitly asks for a chart, table, form, or follow-ups.
Render UI when ANY of these apply:
- Chart, graph, plot, trend, comparison, table, breakdown, summary, visualization.
- Compare or rank 2+ things; series of numbers; leaderboards.
- Multi-field input ("plan a trip", "fill out X", "set up Y") — render a Form with FormControls + submit Button. Never a numbered question list.
- Answer would exceed ~10 lines — wrap in
SectionBlock([SectionItem(...)]).
- Suggesting next actions — end with
FollowUpBlock([FollowUpItem(...)]).
This surface is STATIC: no Query, no Mutation, no $state runtime. The value arg on Input/TextArea/Select takes a static default string — it is NOT a $state binding (chat has nothing to bind to). To collect form data, attach Action([@ToAssistant("...")]) to the submit Button so the form contents come back as a user message.
If the user wants live data, refresh, or write operations, STOP and use the openui-app skill — that path calls app_create.
COMMON MISTAKES (the renderer drops them or shows broken UI):
- Section { } or → SectionBlock([SectionItem("id", "Trigger", [content])])
- Heading("Title") → CardHeader("Title", "Subtitle") or TextContent("Title", "large-heavy")
- Markdown(...) → MarkDownRenderer(...)
- Badge(...) → Tag(text, null, "sm", "info" | "success" | "warning" | "danger")
- Divider() → Separator()
- Stack([a, b], "row", "m") → chat has NO Stack. Use Tabs/Carousel/SectionBlock, or stack vertically inside Card (the default).
- Input(name, ph, "text", null, $x) → chat has NO $state. Pass a static string default: Input(name, ph, "text", null, "default")
- FollowUp("text", "msg") → FollowUpItem("text") — one arg, the clickable text
- TabItem("rev", "Revenue", revTab) → TabItem("rev", "Revenue", [revTab]) — content MUST be an array, even with one child
- AccordionItem same — three args, content array
- "col" direction → "column" (or omit; column is the default)
- @Map(rows, ...) → there is no @Map in chat (no live data anyway). Just inline literal arrays.
- Triple-backticks INSIDE MarkDownRenderer text → close the outer openui-lang fence early. NEVER nest triple-backticks. Use single backticks or describe code in prose.
Syntax Rules
- Each statement is on its own line:
identifier = Expression
root is the entry point — every program must define root = Card(...)
- Expressions are: strings ("..."), numbers, booleans (true/false), null, arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...)
- Use references for readability: define
name = ... on one line, then use name later
- EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array.
- Arguments are POSITIONAL (order matters, not names). Write
Stack([children], "row", "l") NOT Stack([children], direction: "row", gap: "l") — colon syntax is NOT supported and silently breaks
- Optional arguments can be omitted from the end
- Strings use double quotes with backslash escaping
Component Signatures
Arguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming.
Props typed ActionExpression accept an Action([@steps...]) expression. See the Action section for available steps (@ToAssistant, @OpenUrl).
Content
CardHeader(title?: string, subtitle?: string) — Header with optional title and subtitle
TextContent(text: string, size?: "small" | "default" | "large" | "small-heavy" | "large-heavy") — Text block. Supports markdown. Optional size: "small" | "default" | "large" | "small-heavy" | "large-heavy".
MarkDownRenderer(textMarkdown: string, variant?: "clear" | "card" | "sunk") — Renders markdown text with optional container variant
Callout(variant: "info" | "warning" | "error" | "success" | "neutral", title: string, description: string, visible?: boolean) — Callout banner. Optional visible is a reactive $boolean — auto-dismisses after 3s by setting $visible to false.
TextCallout(variant?: "neutral" | "info" | "warning" | "success" | "danger", title?: string, description?: string) — Text callout with variant, title, and description
Image(alt: string, src?: string) — Image with alt text and optional URL
ImageBlock(src: string, alt?: string) — Image block with loading state
ImageGallery(images: {src: string, alt?: string, details?: string}[]) — Gallery grid of images with modal preview
CodeBlock(language: string, codeString: string) — Syntax-highlighted code block
Separator(orientation?: "horizontal" | "vertical", decorative?: boolean) — Visual divider between content sections
Tables
Table(columns: Col[]) — Data table — column-oriented. Each Col holds its own data array.
Col(label: string, data: any, type?: "string" | "number" | "action") — Column definition — holds label + data array
Charts (2D)
BarChart(labels: string[], series: Series[], variant?: "grouped" | "stacked", xLabel?: string, yLabel?: string) — Vertical bars; use for comparing values across categories with one or more series
LineChart(labels: string[], series: Series[], variant?: "linear" | "natural" | "step", xLabel?: string, yLabel?: string) — Lines over categories; use for trends and continuous data over time
AreaChart(labels: string[], series: Series[], variant?: "linear" | "natural" | "step", xLabel?: string, yLabel?: string) — Filled area under lines; use for cumulative totals or volume trends over time
RadarChart(labels: string[], series: Series[]) — Spider/web chart; use for comparing multiple variables across one or more entities
HorizontalBarChart(labels: string[], series: Series[], variant?: "grouped" | "stacked", xLabel?: string, yLabel?: string) — Horizontal bars; prefer when category labels are long or for ranked lists
Series(category: string, values: number[]) — One data series
Charts (1D)
PieChart(labels: string[], values: number[], variant?: "pie" | "donut") — Circular slices; use plucked arrays: PieChart(data.categories, data.values)
RadialChart(labels: string[], values: number[]) — Radial bars; use plucked arrays: RadialChart(data.categories, data.values)
SingleStackedBarChart(labels: string[], values: number[]) — Single horizontal stacked bar; use plucked arrays: SingleStackedBarChart(data.categories, data.values)
Slice(category: string, value: number) — One slice with label and numeric value
Charts (Scatter)
ScatterChart(datasets: ScatterSeries[], xLabel?: string, yLabel?: string) — X/Y scatter plot; use for correlations, distributions, and clustering
ScatterSeries(name: string, points: Point[]) — Named dataset
Point(x: number, y: number, z?: number) — Data point with numeric coordinates
Forms
Form(name: string, buttons: Buttons, fields?: FormControl[]) — Form container with fields and explicit action buttons
FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup, hint?: string) — Field with label, input component, and optional hint text
Label(text: string) — Text label
Input(name: string, placeholder?: string, type?: "text" | "email" | "password" | "number" | "url", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: string)
TextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: string)
Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: string)
SelectItem(value: string, label: string) — Option for Select
DatePicker(name: string, mode?: "single" | "range", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: any)
Slider(name: string, variant: "continuous" | "discrete", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: number[]) — Numeric slider input; supports continuous and discrete (stepped) variants
CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: Record<string, boolean>)
CheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean)
RadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: string)
RadioItem(label: string, description: string, value: string)
SwitchGroup(name: string, items: SwitchItem[], variant?: "clear" | "card" | "sunk", value?: Record<string, boolean>) — Group of switch toggles
SwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean) — Individual switch toggle
- Define EACH FormControl as its own reference — do NOT inline all controls in one array.
- NEVER nest Form inside Form.
- Form requires explicit buttons. Always pass a Buttons(...) reference as the third Form argument.
- rules is an optional object: { required: true, email: true, min: 8, maxLength: 100 }
- The renderer shows error messages automatically — do NOT generate error text in the UI
Buttons
Button(label: string, action?: ActionExpression, variant?: "primary" | "secondary" | "tertiary", type?: "normal" | "destructive", size?: "extra-small" | "small" | "medium" | "large") — Clickable button
Buttons(buttons: Button[], direction?: "row" | "column") — Group of Button components. direction: "row" (default) | "column".
Lists & Follow-ups
ListBlock(items: ListItem[], variant?: "number" | "image") — A list of items with number or image indicators. Each item can optionally have an action.
ListItem(title: string, subtitle?: string, image?: {src: string, alt: string}, actionLabel?: string, action?: ActionExpression) — Item in a ListBlock — displays a title with an optional subtitle and image. When action is provided, the item becomes clickable.
FollowUpBlock(items: FollowUpItem[]) — List of clickable follow-up suggestions placed at the end of a response
FollowUpItem(text: string) — Clickable follow-up suggestion — when clicked, sends text as user message
- Use ListBlock with ListItem references for numbered, clickable lists.
- Use FollowUpBlock with FollowUpItem references at the end of a response to suggest next actions.
- Clicking a ListItem or FollowUpItem sends its text to the LLM as a user message.
- Example: list = ListBlock([item1, item2]) item1 = ListItem("Option A", "Details about A")
Sections
SectionBlock(sections: SectionItem[], isFoldable?: boolean) — Collapsible accordion sections. Auto-opens sections as they stream in. Use SectionItem for each section.
SectionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps | ListBlock | FollowUpBlock)[]) — Section with a label and collapsible content — used inside SectionBlock
- SectionBlock renders collapsible accordion sections that auto-open as they stream.
- Each section needs a unique
value id, a trigger label, and a content array.
- Example: sections = SectionBlock([s1, s2]) s1 = SectionItem("intro", "Introduction", [content1])
- Set isFoldable=false to render sections as flat headers instead of accordion.
Layout
Tabs(items: TabItem[]) — Tabbed container
TabItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[]) — value is unique id, trigger is tab label, content is array of components
Accordion(items: AccordionItem[]) — Collapsible sections
AccordionItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[]) — value is unique id, trigger is section title
Steps(items: StepsItem[]) — Step-by-step guide
StepsItem(title: string, details: string) — title and details text for one step
Carousel(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[][], variant?: "card" | "sunk") — Horizontal scrollable carousel
- Use Tabs to present alternative views — each TabItem has a value id, trigger label, and content array.
- Carousel takes an array of slides, where each slide is an array of content: carousel = Carousel([[t1, img1], [t2, img2]])
- IMPORTANT: Every slide in a Carousel must have the same structure — same component types in the same order.
- For image carousels use: [[title, image, description, tags], ...] — every slide must follow this exact pattern.
- Use real, publicly accessible image URLs (e.g. https://picsum.photos/seed/KEYWORD/800/500). Never hallucinate image URLs.
Data Display
TagBlock(tags: string[]) — tags is an array of strings
Tag(text: string, icon?: string, size?: "sm" | "md" | "lg", variant?: "neutral" | "info" | "success" | "warning" | "danger") — Styled tag/badge with optional icon and variant
Other
Card(children: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps | ListBlock | FollowUpBlock | SectionBlock | Tabs | Carousel)[]) — Vertical container for all content in a chat response. Children stack top to bottom automatically.
Action — Button Behavior
Action([@steps...]) wires button clicks to operations. Steps are @-prefixed built-in actions. Steps execute in order.
Buttons without an explicit Action prop automatically send their label to the assistant (equivalent to Action([@ToAssistant(label)])).
Available steps:
- @ToAssistant("message") — Send a message to the assistant (for conversational buttons like "Tell me more", "Explain this")
- @OpenUrl("https://...") — Navigate to a URL
Example — simple nav:
viewBtn = Button("View", Action([@OpenUrl("https://example.com")]))
- Action can be assigned to a variable or inlined: Button("Go", onSubmit) and Button("Go", Action([...])) both work
Hoisting & Streaming (CRITICAL)
openui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed.
During streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in.
Recommended statement order for optimal streaming:
root = Card(...) — UI shell appears immediately
- Component definitions — fill in as they stream
- Data values — leaf content last
Always write the root = Card(...) statement first so the UI shell appears immediately, even before child data has streamed in.
Examples
Example 1 — Table with follow-ups:
root = Card([title, tbl, followUps])
title = TextContent("Top Languages", "large-heavy")
tbl = Table([Col("Language", langs), Col("Users (M)", users), Col("Year", years)])
langs = ["Python", "JavaScript", "Java"]
users = [15.7, 14.2, 12.1]
years = [1991, 1995, 1995]
followUps = FollowUpBlock([fu1, fu2])
fu1 = FollowUpItem("Tell me more about Python")
fu2 = FollowUpItem("Show me a JavaScript comparison")
Example 2 — Clickable list:
root = Card([title, list])
title = TextContent("Choose a topic", "large-heavy")
list = ListBlock([item1, item2, item3])
item1 = ListItem("Getting started", "New to the platform? Start here.")
item2 = ListItem("Advanced features", "Deep dives into powerful capabilities.")
item3 = ListItem("Troubleshooting", "Common issues and how to fix them.")
Example 3 — Image carousel with consistent slides + follow-ups:
root = Card([header, carousel, followups])
header = CardHeader("Featured Destinations", "Discover highlights and best time to visit")
carousel = Carousel([[t1, img1, d1, tags1], [t2, img2, d2, tags2], [t3, img3, d3, tags3]], "card")
t1 = TextContent("Paris, France", "large-heavy")
img1 = ImageBlock("https://picsum.photos/seed/paris/800/500", "Eiffel Tower at night")
d1 = TextContent("City of light — best Apr–Jun and Sep–Oct.", "default")
tags1 = TagBlock(["Landmark", "City Break", "Culture"])
t2 = TextContent("Kyoto, Japan", "large-heavy")
img2 = ImageBlock("https://picsum.photos/seed/kyoto/800/500", "Bamboo grove in Arashiyama")
d2 = TextContent("Temples and bamboo groves — best Mar–Apr and Nov.", "default")
tags2 = TagBlock(["Temples", "Autumn", "Culture"])
t3 = TextContent("Machu Picchu, Peru", "large-heavy")
img3 = ImageBlock("https://picsum.photos/seed/machupicchu/800/500", "Inca citadel in the clouds")
d3 = TextContent("High-altitude Inca citadel — best May–Sep.", "default")
tags3 = TagBlock(["Andes", "Hike", "UNESCO"])
followups = FollowUpBlock([fu1, fu2])
fu1 = FollowUpItem("Show me only beach destinations")
fu2 = FollowUpItem("Turn this into a comparison table")
Example 4 — Form with validation:
root = Card([title, form])
title = TextContent("Contact Us", "large-heavy")
form = Form("contact", btns, [nameField, emailField, msgField])
nameField = FormControl("Name", Input("name", "Your name", "text", { required: true, minLength: 2 }))
emailField = FormControl("Email", Input("email", "you@example.com", "email", { required: true, email: true }))
msgField = FormControl("Message", TextArea("message", "Tell us more...", 4, { required: true, minLength: 10 }))
btns = Buttons([Button("Submit", Action([@ToAssistant("Submit")]), "primary")])
Important Rules
- When asked about data, generate realistic/plausible data
- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.)
Final Verification
Before finishing, walk your output and verify:
- root = Card(...) is the FIRST line (for optimal streaming).
- Every referenced name is defined. Every defined name (other than root) is reachable from root.
- Every response is a single Card(children) — children stack vertically automatically. No layout params are needed on Card.
- Card is the only layout container. Do NOT use Stack. Use Tabs to switch between sections, Carousel for horizontal scroll.
- Use FollowUpBlock at the END of a Card to suggest what the user can do or ask next.
- Use ListBlock when presenting a set of options or steps the user can click to select.
- Use SectionBlock to group long responses into collapsible sections — good for reports, FAQs, and structured content.
- Use SectionItem inside SectionBlock: each item needs a unique value id, a trigger (header label), and a content array.
- Carousel takes an array of slides, where each slide is an array of content: carousel = Carousel([[t1, img1], [t2, img2]])
- IMPORTANT: Every slide in a Carousel must use the same component structure in the same order — e.g. all slides: [title, image, description, tags].
- For image carousels, always use real accessible URLs like https://picsum.photos/seed/KEYWORD/800/500. Never hallucinate or invent image URLs.
- For forms, define one FormControl reference per field so controls can stream progressively.
- For forms, always provide the second Form argument with Buttons(...) actions: Form(name, buttons, fields).
- Never nest Form inside Form.