Skip to main content
OpenUI is a generative UI library that lets a language model produce complete, interactive UIs in a declarative format called openui-lang. Instead of returning a chat message, the agent returns a component tree with cards, charts, tables, tabs, and forms that the Renderer turns into a real React UI. This integration is well-suited for data-rich outputs like reports, dashboards, and data explorers, where the model is both the data analyst and the UI designer.

How it works

  1. Generate the system prompt: call openuiLibrary.prompt() once at startup; it produces a complete openui-lang reference that the model uses to write valid component trees
  2. Inject on first message: send the system prompt as the opening system message when a new conversation starts
  3. Model writes openui-lang: the model responds with a program like root = Stack([header, kpis, chart]) instead of prose
  4. Render with Renderer: pass the text to OpenUI’s Renderer and the component library; it parses and renders the tree

Installation

OpenUI requires React 19+ and zustand. The frontend code is React-only; the LangGraph agent backend can be written in TypeScript or Python.

Import the component styles

Import OpenUI’s bundled styles in your CSS entry point or directly in your root component:

Generate the system prompt

OpenUI ships a openuiLibrary.prompt() function that generates the complete openui-lang reference, with all component signatures, syntax rules, streaming tips, and examples. Call it once at module load time:
The preamble overrides the default persona. Add additionalRules to inject task-specific constraints:

Inject the system prompt via useStream

Send the system prompt as the first message of every new thread. Check stream.messages.length === 0 to detect a fresh thread and prepend a system message:

Render with the Renderer

Pass the AI message’s text content directly to Renderer along with openuiLibrary:
Pass isStreaming={true} during the active stream so the Renderer handles unresolved references gracefully as definitions arrive.

The openui-lang format

The model writes a program rather than a JSON spec. Every statement is an assignment; root is the entry point. The official prompt teaches the model this format, including hoisting — writing root first so the UI shell appears immediately:
With hoisting enabled (recommended), the root line is written first so the page structure appears immediately and each section fills in as the model defines it.

Progressive rendering utilities

Wiring useStream to Renderer directly causes results in re-rendering on every streaming token and produces hundreds of no-op re-parses per response. This causes chart components to crash when their data hasn’t arrived yet. The utilities below solve these problems: Copy the full block into your project and pass stable to <Renderer>:

Follow-up queries

OpenUI’s Button component supports a continue_conversation action type. When the user clicks a follow-up button, Renderer fires onAction and the AIMessageView above submits the button’s label as the next user message, exactly the same code path as typing in the input. Add an “Explore Further” section to every report via additionalRules in the system prompt:

Best practices

  • Generate the system prompt at module load: not inside a React component; the prompt is several kilobytes and should be computed once
  • Inject the system prompt only on fresh threads: check stream.messages.length === 0 and skip injection on subsequent turns to avoid duplicating the prompt in the thread history
  • Use hoisting order: write root = Stack([...]) first; the UI shell appears immediately and sections fill in progressively as the model defines each one
  • Gate on complete statements: avoid re-rendering the Renderer on every token; update only when a full statement (name = ComponentCall(...)) has arrived
  • Verify chart data before rendering: chart components need their Series and label arrays defined before they’re included in the stable snapshot
  • Keep camelCase variable names: the openui-lang parser only accepts camelCase identifiers; reinforce this in the system prompt’s additionalRules