OpenUI

Custom chat UI

Render OpenUI Lang in a chat interface you built yourself.

This guide assumes the application already has a working chat interface that renders text or Markdown, and that its backend streams OpenUI Lang. If it doesn't yet, follow Set up UI generation first.

The message list, composer, conversation state, and transport stay the same. Only the assistant message changes.

Install

Skip this step if the chat UI and the backend share a package and you already installed these in Set up UI generation.

pnpm add @openuidev/react-lang @openuidev/react-ui

@openuidev/react-lang renders OpenUI Lang. @openuidev/react-ui provides the built-in component library and its styles; if the agent uses only your own components, skip it and the styles import below.

When using the built-in library, import its styles once, from the application's global stylesheet:

app/globals.css
@import "@openuidev/react-ui/styles/index.css";

The examples below import library from src/lib/openui.tsx, the file created in Set up UI generation. If the chat UI lives in a separate package, define the same library there so it renders every component the agent can write.

Render OpenUI Lang

In the assistant message component, render the streamed response with <Renderer /> instead of the Markdown renderer:

src/components/assistant-message.tsx
"use client";

import { Renderer } from "@openuidev/react-lang";
import { library } from "@/lib/openui";

export function AssistantMessage({
  content,
  isStreaming,
}: {
  content: string;
  isStreaming: boolean;
}) {
  return <Renderer response={content} library={library} isStreaming={isStreaming} />;
}

content is the accumulated response text. Keep isStreaming true until the response stream closes so the renderer displays the interface progressively.

See The Renderer for all of its props, including error handling.

Handle actions

Buttons, follow-ups, and forms in a generated interface report clicks through the renderer's onAction callback. Without it, they do nothing. Handle the two built-in action types:

  • continue_conversation: send a new user turn. Include the form values so the model knows what the user entered, as described in Conversation history.
  • open_url: open event.params.url in a new tab.

Pass the chat's existing send function to the assistant message, and call it from onAction:

src/components/assistant-message.tsx
"use client";

import { BuiltinActionType, Renderer, type ActionEvent } from "@openuidev/react-lang";
import { library } from "@/lib/openui";

function toModelMessage(event: ActionEvent) {
  const lines = [`User clicked: ${event.humanFriendlyMessage}`];
  if (event.formState) lines.push(`Form values: ${JSON.stringify(event.formState)}`);
  return lines.join("\n");
}

export function AssistantMessage({
  content,
  isStreaming,
  onSend,
}: {
  content: string;
  isStreaming: boolean;
  onSend: (message: { display: string; content: string }) => void;
}) {
  const onAction = (event: ActionEvent) => {
    if (event.type === BuiltinActionType.ContinueConversation) {
      onSend({ display: event.humanFriendlyMessage, content: toModelMessage(event) });
    } else if (event.type === BuiltinActionType.OpenUrl) {
      window.open(String(event.params?.url), "_blank", "noopener,noreferrer");
    }
  };

  return (
    <Renderer response={content} library={library} isStreaming={isStreaming} onAction={onAction} />
  );
}

onSend stands in for however the chat sends a message today. Show display in the message list, and send content to the backend so the model sees the form values.

On this page