Components as Tools

Let your agent render rich React components directly in the chat by calling them as tools.

"use client";import React from "react";import {  CopilotChat,  CopilotKit,  useComponent,} from "@copilotkit/react-core/v2";import { BarChart, barChartPropsSchema } from "./bar-chart";import { PieChart, pieChartPropsSchema } from "./pie-chart";import { useSuggestions } from "./suggestions";function Chat() {  useComponent({    name: "render_bar_chart",    description: "Display a bar chart with labeled numeric values.",    parameters: barChartPropsSchema,    render: BarChart,  });  useComponent({    name: "render_pie_chart",    description: "Display a pie chart with labeled numeric values.",    parameters: pieChartPropsSchema,    render: PieChart,  });  useSuggestions();  return (    <div className="flex justify-center items-center h-screen w-full">      <div className="h-full w-full max-w-4xl">        <CopilotChat          agentId="gen-ui-tool-based"          className="h-full rounded-2xl"        />      </div>    </div>  );}export default function ControlledGenUiDemo() {  return (    <CopilotKit runtimeUrl="/api/copilotkit" agent="gen-ui-tool-based">      <Chat />    </CopilotKit>  );}

What is this?#

Tool-based Generative UI is the simplest form of Generative UI: you register a React component with useComponent, and CopilotKit exposes it to the agent as a tool. When the agent calls the tool, CopilotKit renders your component inline in the chat, passing the tool's arguments straight through as typed props.

Unlike tool rendering, which wraps a real backend tool in a custom UI, tool-based GenUI is the component. There is no handler, no user interaction, no server-side execution. The agent decides when to show it, populates the data, and CopilotKit paints it.

When should I use this?#

Use useComponent when you want to:

  • Display rich UI (cards, charts, tables, dashboards) inline in the chat
  • Show structured data the agent has derived from its reasoning
  • Render previews, status indicators, or visual summaries
  • Let the agent present information beyond plain text

For components that need user interaction, see Human-in-the-loop. For operational transparency around a real backend tool, see Tool rendering.

How it works in code#

Nothing to wire in config mode

A BuiltInAgent built from configuration merges the request's tool definitions into the tool list it hands the model, so the agent declares none of its own. A component registered with useComponent reaches the model by name with no extra wiring.

app/api/copilotkit/[[...slug]]/route.ts
import { BuiltInAgent } from "@copilotkit/runtime/v2";

const builtInAgent = new BuiltInAgent({
  model: "openai:gpt-5.4-mini",
  prompt: SYSTEM_PROMPT,
});

Keep the useComponent name distinct from every tool in config.tools. The runtime layers the configured tools over the forwarded ones, so a shared name resolves to the backend tool and the component never renders.

In factory mode you pass the tools yourself

A factory owns the model call, so BuiltInAgent ignores config.tools and forwards nothing on your behalf. Read the tools off input and pass them to the model, or the component never renders.

app/api/copilotkit/factory.ts
import {
  BuiltInAgent,
  convertMessagesToVercelAISDKMessages,
  convertToolsToVercelAITools,
} from "@copilotkit/runtime/v2";
import { openai } from "@ai-sdk/openai";
import { streamText } from "ai";

const agent = new BuiltInAgent({
  type: "aisdk",
  factory: ({ input, abortSignal }) =>
    streamText({
      model: openai("gpt-4o"),
      messages: convertMessagesToVercelAISDKMessages(input.messages),
      tools: convertToolsToVercelAITools(input.tools),
      abortSignal,
    }),
});

The factory owns precedence here. Nothing merges the two lists for you, so a backend tool and a useComponent registration that share a name is your conflict to resolve. Keep the names distinct.

Tell the model when to call it

This is the part that is easy to miss. The tool arrives on every run, but a model with no instruction about it will answer in prose and never call it. Name the tool in the prompt and say what it is for.

app/api/copilotkit/prompts.ts
export const SYSTEM_PROMPT = `You are a data visualization assistant.

When the user asks for a chart, call \`render_bar_chart\` with a concise
title and a \`data\` array of \`{label, value}\` items.

If the user names a subject but supplies no numbers, invent plausible
sample values and render the chart on the first turn. Never reply with a
clarifying question asking for the data.`;

Import the React hook and Zod in the component that registers the tool. This also applies to the built-in agent, which needs no backend tool-registration step.

import { useComponent } from "@copilotkit/react-core/v2";
import { z } from "zod";

useComponent takes a name, a Zod schema for its props, and the component to render. The runtime registers it as a frontend tool so the agent can discover it, and the schema becomes that tool's parameter definition — it is what tells the model which arguments to send.

parameters is optional, but leaving it out advertises the tool with an empty parameter schema ({ "type": "object", "properties": {} }). The model then has nothing to fill in, so it calls the tool with no arguments and your component renders with no props. Pass a schema for any component that needs data.

page.tsx
  useComponent({    name: "render_bar_chart",    description: "Display a bar chart with labeled numeric values.",    parameters: barChartPropsSchema,    render: BarChart,  });

The component itself is ordinary React: it reads only its props and can stream in as the agent fills the payload. The example above uses Recharts for the bar chart; it doesn't know anything about CopilotKit.

The name you pass to useComponent is what the agent sees as the tool name. Make it a verb like render_bar_chart or show_weather so the LLM reliably picks it when the user asks for that visualization.

Rendering in a headless chat#

CopilotKit's built-in chat components paint registered components for you. A headless or custom chat renders the message list itself, so nothing paints a tool call unless you render it — the component is registered and the agent calls it, but the chat stays empty.

Render the tool calls on each assistant message with CopilotChatToolCallsView:

import { CopilotChatToolCallsView } from "@copilotkit/react-core/v2";

<CopilotChatToolCallsView message={assistantMessage} messages={allMessages} />;

It looks up the sibling tool-role message for each tool call and hands both to the registered renderer. For finer placement, call useRenderToolCall() and paint each tool call yourself — see Headless UI.

Choose your AI backend

See Integrations for all available frameworks (generative-ui/tool-based).