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).

Shellterminal
# 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-number

Scaffolding 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.

TSXlib/beui-library.tsx
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.

TSXlib/beui-library.tsx
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.

TSXapp/api/generate/route.ts
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.

TSXgenerative-response.tsx
"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

  • OpenUI

    The generative UI framework and its OpenUI Lang.

    Open
  • Defining components

    Full defineComponent / createLibrary reference.

    Open
  • shadcn chat example

    The registry-library pattern this guide follows.

    Open

Other generative UI frameworks that consume shadcn registries can pull beUI the same way — more integration guides to come.