Integration
Use beUI with OpenUI
OpenUI is a generative UI framework: instead of returning markdown, the model emits an abstract UI tree (OpenUI Lang) and a React runtime maps every node to a component you register. This OpenUI React integration turns beUI into a custom OpenUI component library, so each generated response uses real, animated components—and only the components you allow.
Install
You need OpenUI's React runtime and the beUI components you want to expose. Pull beUI source with the shadcn registry (see the AI Agents guide for other install paths).
# OpenUI runtime, server prompt generation, and schema validation
npm install @openuidev/react-lang @openuidev/lang-core zod
# Your model SDK (any provider works)
npm install openai
# Pull the beUI components you want to expose (shadcn registry)
npx shadcn@latest add @beui/button @beui/animated-badge @beui/animated-numberScaffolding from scratch? Run npx @openuidev/cli@latest create for a working streaming app, then swap its default library for the beUI one below.
Register components
defineComponent maps one OpenUI Lang node to a beUI component. The Zod props schema validates the model's output as it streams, the description is injected into the system prompt so the model learns each component's intent, and useTriggerAction keeps rendered controls interactive. Describe a container's children as a union of the registered .ref schemas so nesting is both validated and advertised to the model.
import { defineComponent, useTriggerAction } from "@openuidev/react-lang";
import { z } from "zod/v4";
import { Button } from "@/components/motion/button";
import { AnimatedBadge } from "@/components/motion/animated-badge";
import { AnimatedNumber } from "@/components/motion/animated-number";
// beUI Button — the model picks a variant and an optional press ripple.
// `useTriggerAction` keeps it live: pressing it sends `action` back to the
// model, so generated buttons continue the conversation instead of sitting inert.
const BeButton = defineComponent({
name: "Button",
description:
"Spring-pressed action button. `action` is the message sent to the model when pressed.",
props: z.object({
label: z.string(),
action: z.string(),
variant: z.enum(["primary", "secondary", "ghost", "outline"]).default("primary"),
ripple: z.boolean().default(false),
}),
component: ({ props }) => {
const triggerAction = useTriggerAction();
return (
<Button
variant={props.variant}
ripple={props.ripple}
onClick={() => triggerAction(props.action)}
>
{props.label}
</Button>
);
},
});
// beUI status pill with a pulse and animated state icon.
const BeBadge = defineComponent({
name: "Badge",
description:
"Status pill. Pick a status colour; set pulse for live or in-progress states.",
props: z.object({
label: z.string(),
status: z
.enum(["neutral", "info", "success", "warning", "danger", "loading"])
.default("neutral"),
pulse: z.boolean().default(false),
}),
component: ({ props }) => (
<AnimatedBadge status={props.status} pulse={props.pulse}>
{props.label}
</AnimatedBadge>
),
});
// beUI spring count-up for a single metric.
const BeStat = defineComponent({
name: "Stat",
description: "A single numeric metric that springs up from zero when shown.",
props: z.object({ label: z.string(), value: z.number() }),
component: ({ props }) => (
<div className="rounded-xl border border-border bg-card p-4">
<p className="text-sm text-muted-foreground">{props.label}</p>
<AnimatedNumber value={props.value} className="text-2xl font-semibold" />
</div>
),
});
// The root node stacks the components above. Describe its children as a union
// of each component's `.ref` (declared after the components exist): the runtime
// validates what may nest here, and the model sees exactly which nodes are
// allowed inside — both of which `z.any()` would throw away.
const StackChild = z.union([BeButton.ref, BeBadge.ref, BeStat.ref]);
const Stack = defineComponent({
name: "Stack",
description: "Vertical container. Children stack top to bottom with spacing.",
props: z.object({ children: z.array(StackChild) }),
component: ({ props, renderNode }) => (
<div className="flex flex-col gap-3">{renderNode(props.children)}</div>
),
});Assemble the library
createLibrary collects the definitions, names the root node, and organises the prompt into componentGroups with notes that steer how the model reaches for each one.
import { createLibrary } from "@openuidev/react-lang";
export const beuiLibrary = createLibrary({
root: "Stack",
components: [Stack, BeButton, BeBadge, BeStat],
componentGroups: [
{
name: "Layout",
components: ["Stack"],
notes: ["Every response is a single Stack at the root."],
},
{
name: "beUI motion",
components: ["Button", "Badge", "Stat"],
notes: [
"Use Badge with pulse for live or streaming status.",
"One Button per response, as the primary action.",
],
},
],
});Generate the prompt
The client renders OpenUI Lang, but the model has to produce it. In the CLI, call npx @openuidev/cli@latest generate --spec ./lib/beui-library.tsx --out ./lib/generated/beui-library.spec.json to build the library spec. The backend combines its component signatures, descriptions, and nesting constraints with the OpenUI Lang grammar, then streams the model's reply back to the browser.
import OpenAI from "openai";
import { generateSystemPrompt } from "@openuidev/lang-core";
import beuiLibrarySpec from "@/lib/generated/beui-library.spec.json";
const openai = new OpenAI();
// The generated library spec teaches the model which OpenUI Lang it may emit.
export async function POST(req: Request) {
const { messages } = await req.json();
const stream = await openai.responses.create({
model: "gpt-5.6-sol",
stream: true,
store: false,
// Grammar + a signature and description for every registered component,
// so the model only ever emits nodes your library defines.
instructions: generateSystemPrompt({ library: beuiLibrarySpec }),
input: messages,
});
const response = new ReadableStream<Uint8Array>({
async start(controller) {
const encoder = new TextEncoder();
try {
for await (const event of stream) {
if (event.type === "response.output_text.delta") {
controller.enqueue(encoder.encode(event.delta));
}
}
controller.close();
} catch (error) {
controller.error(error);
}
},
});
return new Response(response, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}Run that command when the library changes. It serializes the library to JSON at build time, and the route imports that generated file through the same @/lib/generated path, so the server bundle never imports your client components.
Render the stream
Pass the library to <Renderer> on the client. It parses the OpenUI Lang your server streams and paints beUI components progressively as tokens arrive. Wire onAction to send a pressed button's message back to the model and continue the loop.
"use client";
import { Renderer } from "@openuidev/react-lang";
import { beuiLibrary } from "@/lib/beui-library";
// `response` is the OpenUI Lang your server streams from the model.
export function GenerativeResponse({
response,
isStreaming,
onSend,
}: {
response: string | null;
isStreaming: boolean;
onSend: (message: string) => void;
}) {
return (
<Renderer
response={response}
library={beuiLibrary}
isStreaming={isStreaming}
// A registered Button was pressed — send its message back to the
// model to continue the conversation.
onAction={(event) => onSend(event.humanFriendlyMessage)}
/>
);
}Why beUI fits
beUI components own their files and ship through the registry, so there's no runtime to bolt on — the library above is the integration. They use semantic controls and shadcn tokens, so model-generated UIs inherit the host app's theme without extra wiring.
Resources
- OpenUIOpen
The generative UI framework and its OpenUI Lang.
- Defining componentsOpen
Full defineComponent / createLibrary reference.
- shadcn chat exampleOpen
The registry-library pattern this guide follows.
Other generative UI frameworks that consume shadcn registries can pull beUI the same way — more integration guides to come.