Display components

Register React components that your agent can render in the chat.


"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?#

Render-only generative UI lets you register React components as tools your agent can invoke. When the agent calls the tool, CopilotKit renders your component directly in the chat with the tool's arguments as props; no handler logic or user interaction required.


useComponent({
name: "showChart",
description: "Populate data and show the user a chart",
parameters: ChartProps,
render: Chart
});

export const ChartProps = z.object({
  title: z.string(),
  data: z.array(z.object({ label: z.string(), value: z.number() })),
});

export function Chart({ title, data }: z.infer<typeof ChartProps>) {
  return (
    <div>
      <h3>{title}</h3>
      <ResponsiveContainer width="100%" height={300}>
        <BarChart data={data}>
          <XAxis dataKey="label" /><YAxis /><Tooltip />
          <Bar dataKey="value" fill="#6366f1" />
        </BarChart>
      </ResponsiveContainer>
    </div>
  );
}

When should I use this?#

Use render-only generative UI when you want to:

  • Display rich UI (cards, charts, tables) inline in the chat
  • Show structured data from agent responses
  • Render previews, status indicators, or visual feedback
  • Let the agent present information beyond plain text

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.

src/agents/chart_flow.py
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.

src/agents/chart_flow.py
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.

The renderer component receives the tool's arguments as typed props and mounts inline in the chat. Below is the chart renderer wired up in the canonical demo — the agent emits the data, the component draws it.

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