---
title: Defining Components
description: Define OpenUI components with Zod and React renderers.
---

Use `defineComponent(...)` to register each component and `createLibrary(...)` to assemble the library.

## Core API

```tsx
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`

1. `name`: component call name in OpenUI Lang.
2. `props`: `z.object(...)` schema. Key order defines positional argument order.
3. `description`: used in prompt component signature lines.
4. `component`: React renderer receiving `{ props, renderNode }`.

## Nesting pattern with `.ref`

```tsx
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.

```tsx
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`.

```tsx
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>(...)`.

```ts
const library = createLibrary({
  root: "Stack", // → prompt tells LLM: "every program must define root = Stack(...)"
  components: [Stack, Card, TextContent],
});
```

This serves two purposes:

1. **Constrains the LLM**: the model always wraps its output in a known top-level component, making output predictable.
2. **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

```ts
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:

```ts
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:

```ts
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](/docs/openui-lang/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](/docs/openui-lang/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](#grouping-components-in-prompt-output) above.
