Components as Tools
Let your agent render rich React components directly in the chat by calling them as tools.
"use client";import React, { useState } from "react";import { CopilotKit } from "@copilotkit/react-core";import { CopilotSidebar, useFrontendTool, useConfigureSuggestions,} from "@copilotkit/react-core/v2";import { z } from "zod";interface Haiku { japanese: string[]; english: string[]; image_name: string | null; gradient: string;}export default function GenUiToolBasedDemo() { return ( <CopilotKit runtimeUrl="/api/copilotkit" agent="gen-ui-tool-based"> <SidebarWithSuggestions /> <HaikuDisplay /> </CopilotKit> );}function SidebarWithSuggestions() { useConfigureSuggestions({ suggestions: [ { title: "Nature Haiku", message: "Write me a haiku about nature." }, { title: "Ocean Haiku", message: "Create a haiku about the ocean." }, { title: "Spring Haiku", message: "Generate a haiku about spring." }, ], available: "always", }); return ( <CopilotSidebar defaultOpen={true} labels={{ modalHeaderTitle: "Haiku Generator", }} /> );}const VALID_IMAGE_NAMES = [ "Osaka_Castle_Turret_Stone_Wall_Pine_Trees_Daytime.jpg", "Tokyo_Skyline_Night_Tokyo_Tower_Mount_Fuji_View.jpg", "Itsukushima_Shrine_Miyajima_Floating_Torii_Gate_Sunset_Long_Exposure.jpg", "Takachiho_Gorge_Waterfall_River_Lush_Greenery_Japan.jpg", "Bonsai_Tree_Potted_Japanese_Art_Green_Foliage.jpeg", "Shirakawa-go_Gassho-zukuri_Thatched_Roof_Village_Aerial_View.jpg", "Ginkaku-ji_Silver_Pavilion_Kyoto_Japanese_Garden_Pond_Reflection.jpg", "Senso-ji_Temple_Asakusa_Cherry_Blossoms_Kimono_Umbrella.jpg", "Cherry_Blossoms_Sakura_Night_View_City_Lights_Japan.jpg", "Mount_Fuji_Lake_Reflection_Cherry_Blossoms_Sakura_Spring.jpg",];function HaikuDisplay() { const [haikus, setHaikus] = useState<Haiku[]>([ { japanese: ["仮の句よ", "まっさらながら", "花を呼ぶ"], english: [ "A placeholder verse--", "even in a blank canvas,", "it beckons flowers.", ], image_name: null, gradient: "", }, ]); useFrontendTool( { name: "generate_haiku", parameters: z.object({ japanese: z.array(z.string()).describe("3 lines of haiku in Japanese"), english: z .array(z.string()) .describe("3 lines of haiku translated to English"), image_name: z .string() .describe( `One relevant image name from: ${VALID_IMAGE_NAMES.join(", ")}`, ), gradient: z.string().describe("CSS Gradient color for the background"), }), followUp: false, handler: async ({ japanese, english, image_name, gradient, }: { japanese: string[]; english: string[]; image_name: string; gradient: string; }) => { const newHaiku: Haiku = { japanese: japanese || [], english: english || [], image_name: image_name || null, gradient: gradient || "", }; setHaikus((prev) => [ newHaiku, ...prev.filter((h) => h.english[0] !== "A placeholder verse--"), ]); return "Haiku generated!"; }, render: ({ args }: { args: Partial<Haiku> }) => { if (!args.japanese) return <></>; return <HaikuCard haiku={args as Haiku} />; }, }, [haikus], ); return ( <div className="relative flex items-center justify-center h-full w-full"> <div className="px-20 py-12 w-full max-w-4xl"> <div className="space-y-6"> {haikus.map((haiku, index) => ( <HaikuCard key={index} haiku={haiku} /> ))} </div> </div> </div> );}function HaikuCard({ haiku }: { haiku: Partial<Haiku> }) { return ( <div data-testid="haiku-card" style={{ background: haiku.gradient }} className="relative bg-gradient-to-br from-slate-50 to-blue-50 rounded-2xl my-6 p-8 max-w-2xl border border-slate-200 overflow-hidden" > <div className="absolute top-0 right-0 w-64 h-64 bg-gradient-to-br from-blue-400/10 to-purple-400/10 rounded-full blur-3xl -z-0" /> <div className="absolute bottom-0 left-0 w-48 h-48 bg-gradient-to-tr from-indigo-400/10 to-pink-400/10 rounded-full blur-3xl -z-0" /> <div className="relative z-10 flex flex-col items-center space-y-6"> {haiku.japanese?.map((line, index) => ( <div key={index} className="flex flex-col items-center text-center space-y-2" style={{ animationDelay: `${index * 100}ms` }} > <p data-testid="haiku-japanese-line" className="font-serif font-bold text-4xl md:text-5xl bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-transparent tracking-wide" > {line} </p> <p data-testid="haiku-english-line" className="font-light text-base md:text-lg text-slate-600 italic max-w-md" > {haiku.english?.[index]} </p> </div> ))} </div> {haiku.image_name && ( <div className="relative z-10 mt-8 pt-8 border-t border-slate-200"> <div className="relative group overflow-hidden rounded-2xl shadow-xl"> <img data-testid="haiku-image" src={`/images/${haiku.image_name}`} alt={haiku.image_name} className="object-cover w-full h-64 md:h-80 transform transition-transform duration-500 group-hover:scale-105" /> </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.
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.
