OpenUI

CopilotKit

Render OpenUI Lang in an existing CopilotKit chat.

This guide assumes the application already has a working CopilotKit chat built with CopilotChat, and that its agent responds with OpenUI Lang. If it doesn't yet, follow Set up UI generation first.

The change is in one place: CopilotKit's assistantMessage.markdownRenderer slot. The chat, agent, and runtime stay the same.

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

Pass the OpenUI renderer as the assistant message's Markdown renderer:

src/components/chat.tsx
"use client";

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

function OpenUIRenderer({ content }: { content: string }) {
  return <Renderer response={content} library={library} />;
}

export function Chat() {
  return (
    <CopilotChat
      messageView={{
        assistantMessage: {
          markdownRenderer: OpenUIRenderer,
        },
      }}
    />
  );
}

content is the accumulated response supplied by CopilotKit. The renderer re-parses it as it grows, so the interface builds up while the response streams.

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.

Add the user turn to the agent and run it, as described in CopilotKit's programmatic control guide:

src/components/chat.tsx
"use client";

import { CopilotChat, useAgent, useCopilotKit } from "@copilotkit/react-core/v2";
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");
}

function OpenUIRenderer({ content }: { content: string }) {
  const { agent } = useAgent();
  const { copilotkit } = useCopilotKit();

  const onAction = (event: ActionEvent) => {
    if (event.type === BuiltinActionType.ContinueConversation) {
      agent.addMessage({ id: crypto.randomUUID(), role: "user", content: toModelMessage(event) });
      void copilotkit.runAgent({ agent });
    } else if (event.type === BuiltinActionType.OpenUrl) {
      window.open(String(event.params?.url), "_blank", "noopener,noreferrer");
    }
  };

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

export function Chat() {
  return (
    <CopilotChat
      messageView={{
        assistantMessage: {
          markdownRenderer: OpenUIRenderer,
        },
      }}
    />
  );
}

copilotkit.runAgent is the same entry point CopilotChat uses when the user sends a message, so the new turn streams into the chat as usual.

On this page