thesys|
OpenUI

Defining Components

Learn how to define components for OpenUI using Zod schemas.

To let the LLM render UI, you don't write complex prompt instructions manually. Instead, you define a Library using Zod schemas. OpenUI converts these schemas into the system prompt that teaches the model how to use your components.

The defineComponent + createLibrary API

Every component is first defined with defineComponent, then collected into a library with createLibrary.

import { defineComponent, createLibrary } from '@openuidev/lang-react';
import { z } from 'zod';

const MyCard = defineComponent({
  name: 'MyCard',
  description: 'Displays a titled content card.',
  props: z.object({
    title: z.string(),
  }),
  component: ({ props }) => <div>{props.title}</div>,
});

export const myLibrary = createLibrary({
  components: [MyCard],
});

The Component Definition

Every call to defineComponent needs four things:

  1. name: The identifier the LLM will use in generated code.
  2. props (Zod Schema): Defines the data structure and types.
  3. description: A natural language explanation for the LLM.
  4. component (React): The actual React implementation.

Example: A "Stat Card"

Let's build a simple card that shows a metric (like "Revenue") and a trend.

import { defineComponent, createLibrary } from '@openuidev/lang-react';
import { z } from 'zod';

// 1. Define with defineComponent
// The order of keys in z.object determines the argument order for the LLM.
const StatCard = defineComponent({
  name: 'StatCard',
  description: 'Displays a key metric with a trend indicator.',
  props: z.object({
    label: z.string().describe("The name of the metric, e.g. 'Daily Active Users'"),
    value: z.string().describe("The formatted value, e.g. '1,234'"),
    trend: z.enum(['up', 'down', 'neutral']).describe('Visual indicator direction'),
  }),
  component: ({ props }) => (
    <div className="p-4 border rounded shadow-sm bg-white dark:bg-slate-800">
      <div className="text-sm text-gray-500">{props.label}</div>
      <div className="text-2xl font-bold">{props.value}</div>
      <span className={`trend-${props.trend}`} />
    </div>
  ),
});

// 2. Collect into a library
export const myLibrary = createLibrary({
  components: [StatCard],
});

Best Practices for Zod Schemas

The quality of your Zod schema directly impacts the quality of the LLM's output.

1. Use describe() everywhere

The .describe() string is injected into the system prompt. Treat it as a mini-instruction to the model.

// ❌ Bad
z.string()

// ✅ Good
z.string().describe("ISO 8601 date string, e.g. '2023-01-01'")

2. Prefer Enums over Strings

If a prop has a finite set of options, use z.enum(). This prevents the LLM from hallucinating invalid variants (e.g., passing "red" instead of "danger").

// ❌ Risky
variant: z.string()

// ✅ Safe
variant: z.enum(['primary', 'secondary', 'ghost'])

3. Key Order Matters

OpenUI Lang uses positional arguments to save tokens. The order of keys in your z.object definition determines the order the LLM must generate them.

// Definition
z.object({
  title: z.string(),
  isActive: z.boolean()
})

// LLM Output (OpenUI Lang)
// card = Card("Title First", true)

If you change the order of keys in your schema, the generated system prompt changes. Ensure your LLM session is refreshed if you modify schemas during development.

Nesting Components

Components can reference each other using .ref. Define child components first, then use ChildComponent.ref in parent schemas.

import { defineComponent, createLibrary } from '@openuidev/lang-react';
import { useRenderNode } from '@openuidev/lang-react';
import { z } from 'zod';

const StatCard = defineComponent({
  name: 'StatCard',
  description: 'A metric card.',
  props: z.object({ label: z.string(), value: z.string() }),
  component: ({ props }) => <div>{props.label}: {props.value}</div>,
});

const Dashboard = defineComponent({
  name: 'Dashboard',
  description: 'A dashboard containing stat cards.',
  props: z.object({
    cards: z.array(StatCard.ref),
  }),
  component: ({ props, renderNode }) => (
    <div className="grid grid-cols-3 gap-4">
      {props.cards.map((card, i) => renderNode(card))}
    </div>
  ),
});

export const myLibrary = createLibrary({
  components: [StatCard, Dashboard],
});

Grouping Components

As your library grows, you can organize components into logical groups using componentGroups. This helps structure the system prompt, making it easier for the LLM to find the right tool.

export const myLibrary = createLibrary({
  componentGroups: [
    { name: 'Charts', components: ['BarChart', 'LineChart'] },
    { name: 'Forms', components: ['Input', 'Select', 'Button'] },
  ],
  components: [BarChart, LineChart, Input, Select, Button],
});

Next Steps

Now that you have defined your library, you need to generate the System Prompt to tell the LLM about it.

On this page