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#
Take the forwarded tools off the Flow's state
A Flow owns its own model call, so unlike a chat agent it has to hand the
forwarded tools to the model itself. Type the Flow on CopilotKitState and
read state.copilotkit.actions — that is where a component registered with
useComponent arrives.
from crewai.flow.flow import Flow, start
from litellm import acompletion
from ag_ui_crewai import CopilotKitState, copilotkit_stream
class ChartFlow(Flow[CopilotKitState]):
@start()
async def chat(self) -> None:
actions = self.state.copilotkit.actions or None
response = await copilotkit_stream(
await acompletion(
model="openai/gpt-4.1-mini",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
*self.state.messages,
],
tools=actions,
parallel_tool_calls=False,
stream=True,
)
)
self.state.messages.append(response.choices[0].message)Wrap the call in copilotkit_stream so the tool call reaches the browser as
it streams. A Flow that returns only when the model is finished renders
nothing until the turn ends.
Decide when the component is required
The Flow controls tool_choice, which is the lever a chat agent does not
have. Forcing the call on the user's turn and leaving it on auto
afterwards is what renders the component immediately and still lets the run
end: the follow-up turn is plain narration once the browser has returned the
result.
on_user_turn = bool(
self.state.messages and self.state.messages[-1].get("role") == "user"
)
tool_choice = "required" if actions and on_user_turn else "auto"Leaving tool_choice on auto for every turn is the usual reason a Flow
answers in prose and the component never appears.
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.
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.