CopilotKit

Components as Tools

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


"use client";import {  CopilotKitProvider,  CopilotChat,  useComponent,} from "@copilotkit/react-core/v2";export default function GenUiToolBased() {  return (    <CopilotKitProvider runtimeUrl="/api/copilotkit" useSingleEndpoint>      <Demo />    </CopilotKitProvider>  );}function Demo() {  useComponent({    name: "generate_haiku",    render: HaikuCard,  });  return (    <main className="p-8">      <h1 className="text-2xl font-semibold mb-4">Tool-Based Generative UI</h1>      <p className="text-sm opacity-70 mb-6">        Try: &ldquo;Write me a haiku about nature.&rdquo; The agent calls the        <code className="mx-1 px-1 bg-gray-100 rounded">generate_haiku</code>        tool and the result renders inline as a typed card.      </p>      <CopilotChat />    </main>  );}/** * HaikuCard — rendered by `useComponent({ name: "generate_haiku" })`. * * `useComponent` passes tool-call arguments directly as React props (via * `useFrontendTool`'s `render: ({ args }) => <Component {...args} />`). * The D5 fixture sends: { japanese, english, image_name, gradient }. */// eslint-disable-next-line @typescript-eslint/no-explicit-anyfunction HaikuCard(props: any) {  const japaneseLines: string[] = props.japanese ?? [];  const englishLines: string[] = props.english ?? props.lines ?? [];  const gradient: string | undefined = props.gradient;  const topic: string | undefined = props.topic;  const hasContent = japaneseLines.length > 0 || englishLines.length > 0;  if (!hasContent) {    return (      <div        data-testid="haiku-card"        className="border rounded p-3 my-2 opacity-70 text-sm"      >        Composing haiku{topic ? ` about ${topic}` : ""}…      </div>    );  }  return (    <div      data-testid="haiku-card"      className="border rounded p-4 my-2 bg-amber-50"      style={gradient ? { background: gradient } : undefined}    >      <div className="font-medium mb-2">Haiku{topic ? ` — ${topic}` : ""}</div>      {japaneseLines.length > 0 && (        <div className="mb-2">          {japaneseLines.map((line: string, i: number) => (            <div              key={i}              data-testid="haiku-japanese-line"              className="text-lg leading-relaxed"            >              {line}            </div>          ))}        </div>      )}      <div className="font-serif italic whitespace-pre-line text-lg leading-relaxed">        {englishLines.map((line: string, i: number) => (          <div key={i} data-testid="haiku-english-line">            {line}          </div>        ))}      </div>    </div>  );}

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#

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 Zod validates the LLM's arguments before they reach your component.

bar-chart-renderer.snippet.tsx
import { useComponent } from "@copilotkit/react-core/v2";import { z } from "zod";// Stand-ins for the locally-authored bar chart component + its prop// schema. In a real page, these live in the demo directory (e.g.// `./bar-chart.tsx` exporting `BarChart` and `barChartPropsSchema`).declare const BarChart: React.ComponentType<{  title: string;  data: { label: string; value: number }[];}>;declare const barChartPropsSchema: z.ZodSchema;export function BarChartRenderer() {  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.