Defining Components
Define OpenUI components with Zod and React renderers.
Use defineComponent(...) to register each component and createLibrary(...) to assemble the library.
Core API
import { defineComponent, createLibrary } from "@openuidev/react-lang";
import { z } from "zod/v4";
const StatCard = defineComponent({
name: "StatCard",
description: "Displays a metric label and value.",
props: z.object({
label: z.string(),
value: z.string(),
}),
component: ({ props }) => (
<div>
<strong>{props.label}</strong>
<div>{props.value}</div>
</div>
),
});
export const myLibrary = createLibrary({
root: "StatCard",
components: [StatCard],
});If you want one import path that works with both zod@3.25.x and zod@4, use import { z } from "zod/v4" for OpenUI component schemas.
Required fields in defineComponent
name: component call name in OpenUI Lang.props:z.object(...)schema. Key order defines positional argument order.description: used in prompt component signature lines.component: React renderer receiving{ props, renderNode }.
Nesting pattern with .ref
import { defineComponent } from "@openuidev/react-lang";
import { z } from "zod/v4";
const Item = defineComponent({
name: "Item",
description: "Simple item",
props: z.object({ label: z.string() }),
component: ({ props }) => <div>{props.label}</div>,
});
const List = defineComponent({
name: "List",
description: "List of items",
props: z.object({
items: z.array(Item.ref),
}),
component: ({ props, renderNode }) => <div>{renderNode(props.items)}</div>,
});Union multiple component types pattern
To define container components that accepts multiple child components, you can use the z.union function to define the child components.
import { defineComponent } from "@openuidev/react-lang";
import { z } from "zod/v4";
const TextBlock = defineComponent({
/* ... */
});
const CalloutBlock = defineComponent({
/* ... */
});
const TabItemSchema = z.object({
value: z.string(),
trigger: z.string(),
content: z.array(z.union([TextBlock.ref, CalloutBlock.ref])),
});Naming reusable helper schemas
Use tagSchemaId(...) when a prop uses a standalone helper schema and you want a readable name in generated prompt signatures instead of any.
import { defineComponent, tagSchemaId } from "@openuidev/react-lang";
import { z } from "zod/v4";
const ActionExpression = z.any();
tagSchemaId(ActionExpression, "ActionExpression");
const Button = defineComponent({
name: "Button",
description: "Triggers an action",
props: z.object({
label: z.string(),
action: ActionExpression.optional(),
}),
component: ({ props }) => <button>{props.label}</button>,
});Without tagSchemaId(...), the generated prompt would fall back to action?: any. Components already get their names automatically through defineComponent(...), so this is only needed for non-component helper schemas.
The root field
The root option in createLibrary specifies which component the LLM must use as the entry point. The generated system prompt instructs the model to always start with root = <RootName>(...).
const library = createLibrary({
root: "Stack", // → prompt tells LLM: "every program must define root = Stack(...)"
components: [Stack, Card, TextContent],
});This serves two purposes:
- Constrains the LLM: the model always wraps its output in a known top-level component, making output predictable.
- Enables streaming: because the root statement comes first, the UI shell renders immediately while child components stream in.
The root must match the name of one of the components in your library. If omitted, the prompt uses "Root" as a placeholder.
For the built-in libraries: openuiLibrary uses Stack (flexible layout container), while openuiChatLibrary uses Card (vertical container optimized for chat responses).
Notes on schema metadata
- Positional mapping is driven by Zod object key order.
- Required/optional state is used by parser validation.
Grouping components in prompt output
const library = createLibrary({
root: "Stack",
components: [
/* ... */
],
componentGroups: [
{ name: "Forms", components: ["Form", "FormControl", "Input", "Button", "Buttons"] },
],
});Why group components?
componentGroups organize the generated system prompt into named sections (e.g., Layout, Forms, Charts). This helps the LLM locate relevant components quickly instead of scanning a flat list. Without groups, all component signatures appear under a single "Ungrouped" heading.
Groups also let you co-locate related components so the LLM understands which components work together (e.g., Form with FormControl, Input, Select).
Adding group notes
Each group can include a notes array. These strings are appended directly after the group's component signatures in the generated prompt. Use notes to give the LLM usage hints and constraints:
componentGroups: [
{
name: "Forms",
components: ["Form", "FormControl", "Input", "TextArea", "Select"],
notes: [
"- Define EACH FormControl as its own reference for progressive streaming.",
"- NEVER nest Form inside Form.",
"- Form requires explicit buttons: Form(name, buttons, fields).",
],
},
{
name: "Layout",
components: ["Stack", "Tabs", "TabItem", "Accordion", "AccordionItem"],
notes: [
'- For grid-like layouts, use Stack with direction "row" and wrap=true.',
],
},
],Notes appear in the prompt output like this:
### Forms
Form(id: string, buttons: Buttons, controls: FormControl[]) — Form container
FormControl(label: string, field: Input | TextArea | Select) — Single field
...
- Define EACH FormControl as its own reference for progressive streaming.
- NEVER nest Form inside Form.
- Form requires explicit buttons: Form(name, buttons, fields).Prompt options
When generating the system prompt, you can pass PromptOptions to customize the output further:
import type { PromptOptions } from "@openuidev/react-lang";
const options: PromptOptions = {
preamble: "You are an assistant that outputs only OpenUI Lang.",
additionalRules: ["Always use Card as the root for chat responses."],
examples: [`root = Stack([title])\ntitle = TextContent("Hello", "large-heavy")`],
};
const prompt = library.prompt(options);See System Prompts for full details on prompt generation.
Best practices for LLM generation
Since LLMs are the ones writing OpenUI Lang, component design choices directly affect generation quality.
Keep schemas flat
Deeply nested object props burn tokens and increase error rates. Prefer multiple simple components over one deeply nested one.
Order Zod keys deliberately
Required props first, optional props last. The most important or distinctive prop should be position 0, since the LLM sees it first during generation.
Use descriptive component names
The LLM picks components by name. PricingTable is clearer than Table3. The description field reinforces this.
Limit library size
Every component adds to the system prompt. Include only components the LLM actually needs for the use case. Fewer components means less confusion and better output.
Use .ref for composition, not deep nesting
z.array(ChildComponent.ref) is the idiomatic way to compose. The LLM generates each child as a separate line, which streams and validates independently.
Provide examples in PromptOptions
One or two concrete examples dramatically improve output quality, especially for complex or unusual component shapes. See System Prompts for details.
Use componentGroups with notes
Group related components and add notes like "Use BarChart for comparisons, LineChart for trends" to guide the LLM's choices. See Grouping components above.