Skip to main content
Generative UI allows you to create custom React components that render dynamically based on agent actions, tool calls, and state updates. This enables rich, interactive experiences that go beyond simple text responses.

Overview

Generative UI in CopilotKit enables you to:
  • Render custom components based on agent tool calls
  • Display real-time progress and status updates
  • Create interactive approval interfaces for human-in-the-loop workflows
  • Stream intermediate state from agents to the UI
  • Build rich, contextual interfaces that update as agents work

What You’ll Learn

This guide teaches you how to:
  • Implement the render function in useFrontendTool
  • Use useRenderActivityMessage for rendering agent activity
  • Create status-aware UI components
  • Build custom generative UI for tool calls
  • Handle different execution states (executing, inProgress, complete, error)

Example: Search Results Component

One of the simplest generative UI examples is rendering search results as they’re being fetched: File: components/generative-ui/SearchResults.tsx
import React from "react";
import { Search, Loader, CheckCircle, AlertCircle } from "lucide-react";

type SearchResultsProps = {
  query: string;
  status: "executing" | "inProgress" | "complete" | "error";
};

export function SearchResults({ query, status }: SearchResultsProps) {
  return (
    <div className="bg-white dark:bg-gray-800 p-3 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm">
      <div className="flex items-center gap-2 mb-2">
        <Search className="h-4 w-4 text-blue-500" />
        <h3 className="text-sm font-medium">Search Results</h3>
      </div>

      <p className="text-xs text-gray-500 dark:text-gray-400 mb-2">
        Query: {query}
      </p>

      {status === "executing" && (
        <div className="flex items-center gap-2 text-xs text-blue-500">
          <Loader className="h-3 w-3 animate-spin" />
          <span>Searching...</span>
        </div>
      )}

      {status === "inProgress" && (
        <div className="flex items-center gap-2 text-xs text-amber-500">
          <Loader className="h-3 w-3 animate-spin" />
          <span>Processing...</span>
        </div>
      )}

      {status === "complete" && (
        <div className="flex items-center gap-2 text-xs text-green-500">
          <CheckCircle className="h-3 w-3" />
          <span>Complete</span>
        </div>
      )}

      {status === "error" && (
        <div className="flex items-center gap-2 text-xs text-red-500">
          <AlertCircle className="h-3 w-3" />
          <span>Error</span>
        </div>
      )}
    </div>
  );
}
Usage:
import { useFrontendTool } from "@copilotkit/react";
import { z } from "zod";
import { SearchResults } from "./generative-ui/SearchResults";

useFrontendTool({
  name: "searchInternet",
  description: "Searches the internet for information",
  parameters: z.object({
    query: z.string().describe("The search query"),
  }),
  render: ({ args, status }) => {
    return (
      <SearchResults 
        query={args.query || "No query provided"} 
        status={status} 
      />
    );
  },
});

Example: Car Selection UI

A more complex example from the State Machine example shows an interactive car selection component: File: components/generative-ui/show-car.tsx
import { Car } from "@/lib/types";
import { ToolCallStatus } from "@copilotkit/react";
import Image from "next/image";

interface ShowCarProps {
  car: Car;
  onSelect: () => void;
  onReject?: () => void;
  status: ToolCallStatus;
  className?: string;
}

export function ShowCar({
  car,
  onSelect,
  onReject,
  status,
  className,
}: ShowCarProps) {
  const carDetails = [
    { label: "Make", value: car.make },
    { label: "Model", value: car.model },
    { label: "Year", value: car.year },
    { label: "Color", value: <ColorDisplay color={car.color} /> },
    { label: "Price", value: `$${car.price?.toLocaleString()}`, bold: true },
  ];

  return (
    <div className={`min-w-[300px] max-w-sm bg-white rounded-xl overflow-hidden ${className}`}>
      <div className="relative aspect-[3/3] w-full overflow-hidden h-[250px]">
        <Image
          width={300}
          height={250}
          src={car?.image?.src || ""}
          alt={car?.image?.alt || ""}
          className="object-cover w-full h-full hover:scale-105 transition-transform"
        />
      </div>

      <div className="space-y-6 pt-4 pb-4">
        <div className="space-y-2 px-6">
          <div className="text-2xl font-semibold text-gray-900">
            {car.year} {car.make} {car.model}
          </div>
          {carDetails.map(({ label, value, bold }) => (
            <div key={label} className="flex justify-between items-center py-1">
              <span className="text-gray-500 text-sm">{label}</span>
              <span className={bold ? "font-semibold text-lg" : "text-sm"}>
                {value}
              </span>
            </div>
          ))}
        </div>

        {status !== ToolCallStatus.Complete && (
          <div className="px-6 pt-2">
            <hr className="mb-4 border-gray-100" />
            <div className="flex gap-3">
              {onReject && (
                <button 
                  className="flex-1 bg-gray-50 text-gray-700 px-6 py-3 rounded-lg"
                  onClick={onReject}
                >
                  Other options
                </button>
              )}
              <button 
                className="flex-1 bg-pink-600 text-white px-6 py-3 rounded-lg"
                onClick={onSelect}
              >
                Select
              </button>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}
Key features:
  • Displays car details in a visually appealing card
  • Shows/hides action buttons based on completion status
  • Provides user interaction callbacks (onSelect, onReject)
  • Adapts to different status states

Streaming Agent State

For more advanced use cases, you can stream intermediate state from your agent to create live updates:

Backend: Emit Intermediate State

In your LangGraph agent (Python example): File: agent/src/search.py
from copilotkit.langchain import copilotkit_customize_config, copilotkit_emit_state

async def search_node(state: AgentState, config: RunnableConfig):
    # Configure which state to emit during tool execution
    config = copilotkit_customize_config(
        config,
        emit_intermediate_state=[{
            "state_key": "search_progress",
            "tool": "search_for_places",
            "tool_argument": "search_progress",
        }],
    )
    
    # Your search logic here...
    state["search_progress"] = [
        {"step": "Finding locations", "status": "in_progress"},
        {"step": "Getting details", "status": "pending"},
    ]
    
    # Emit the state to the frontend
    await copilotkit_emit_state(config, state)
    
    return state

Frontend: Render Streaming State

File: lib/hooks/use-trips.tsx
import { useRenderActivityMessage } from "@copilotkit/react";
import { SearchProgress } from "@/components/SearchProgress";
import { AgentState } from "@/lib/types";

export const TripsProvider = ({ children }: { children: ReactNode }) => {
  useRenderActivityMessage({
    agentId: "travel",
    render: ({ activity }) => {
      const state = activity.state as AgentState;
      if (state?.search_progress && state.search_progress.length > 0) {
        return <SearchProgress progress={state.search_progress} />;
      }
      return null;
    },
  });

  return <>{children}</>;
};
File: components/SearchProgress.tsx
import { Loader, CheckCircle, Circle } from "lucide-react";

interface ProgressStep {
  step: string;
  status: "complete" | "in_progress" | "pending";
}

export function SearchProgress({ progress }: { progress: ProgressStep[] }) {
  return (
    <div className="bg-white p-4 rounded-lg border shadow-sm">
      <h3 className="text-sm font-medium mb-3">Search Progress</h3>
      <div className="space-y-2">
        {progress.map((item, index) => (
          <div key={index} className="flex items-center gap-2">
            {item.status === "complete" && (
              <CheckCircle className="h-4 w-4 text-green-500" />
            )}
            {item.status === "in_progress" && (
              <Loader className="h-4 w-4 text-blue-500 animate-spin" />
            )}
            {item.status === "pending" && (
              <Circle className="h-4 w-4 text-gray-300" />
            )}
            <span className="text-sm">{item.step}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

Render Function Parameters

The render function receives an object with these properties:
{
  args: Partial<T> | T;     // The arguments passed to the tool
  status: ToolCallStatus;   // Current execution status
  result?: string;          // Result after completion (if available)
}

Status Values

  • ToolCallStatus.InProgress - The tool call is in progress (arguments may be partial)
  • ToolCallStatus.Executing - The tool is currently being executed (all arguments present)
  • ToolCallStatus.Complete - The tool has completed successfully
  • (Error states are handled separately)

Best Practices

1. Handle All Status States

Always provide UI feedback for each status state:
render: ({ args, status, result }) => {
  if (status === ToolCallStatus.InProgress) {
    return <LoadingSpinner message="Preparing..." />;
  }
  if (status === ToolCallStatus.Executing) {
    return <LoadingSpinner message="Executing..." />;
  }
  if (status === ToolCallStatus.Complete) {
    return <SuccessView result={result} />;
  }
  return null;
}

2. Use Framer Motion for Smooth Transitions

Animate component entries and state changes:
import { motion } from "framer-motion";

export function AnimatedResult({ data, status }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5 }}
    >
      {/* Your content */}
    </motion.div>
  );
}

3. Provide Clear Visual Feedback

Use icons, colors, and animations to indicate status:
{status === ToolCallStatus.Executing && (
  <div className="flex items-center gap-2 text-blue-500">
    <Loader className="h-4 w-4 animate-spin" />
    <span>Processing...</span>
  </div>
)}

4. Make Interactive Components Responsive

Hide action buttons when completed to avoid duplicate actions:
{status !== ToolCallStatus.Complete && (
  <div className="flex gap-2">
    <button onClick={onApprove}>Approve</button>
    <button onClick={onReject}>Reject</button>
  </div>
)}

Advanced: Multiple Cars with Selection

Render multiple options and handle selection: File: components/generative-ui/show-car.tsx
export function ShowCars({ cars, onSelect, status }: ShowCarsProps) {
  const [selectedCar, setSelectedCar] = useState<Car | null>(null);

  const handleSelect = (car: Car) => {
    setSelectedCar(car);
    onSelect(car);
  };

  return (
    <div className="flex flex-row overflow-x-auto gap-4 py-4">
      {cars.map((car, index) => {
        // Only render selected car after selection
        if (selectedCar && car !== selectedCar) return null;

        return (
          <motion.div
            key={index}
            initial={{ opacity: 0, y: 20 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ duration: 0.5, delay: index * 0.2 }}
          >
            <ShowCar
              car={car}
              onSelect={() => handleSelect(car)}
              status={status}
            />
          </motion.div>
        );
      })}
    </div>
  );
}
Usage in action:
import { useFrontendTool } from "@copilotkit/react";
import { z } from "zod";

useFrontendTool({
  name: "showCarOptions",
  description: "Show car options to the user",
  parameters: z.object({
    cars: z.array(z.object({
      make: z.string(),
      model: z.string(),
      year: z.number(),
    })).describe("Array of car options"),
  }),
  render: ({ args, status }) => {
    return (
      <ShowCars
        cars={args.cars || []}
        status={status}
        onSelect={(car) => {
          console.log("Selected car:", car);
        }}
      />
    );
  },
});

Real-World Examples

Research Canvas

The Research Canvas example uses generative UI to display research progress and results in real-time as the agent conducts research. Source: GitHub

State Machine Copilot

The State Machine example uses multiple generative UI components for different stages:
  • Contact information forms
  • Car selection cards
  • Financing options
  • Payment cards
  • Order confirmation
Source: GitHub

Travel Planner

The Travel Planner uses generative UI for:
  • Trip addition/editing approval interfaces
  • Search progress indicators
  • Map marker updates
Source: GitHub

Next Steps

Human-in-the-Loop

Learn how to use generative UI for approval workflows

Shared State

Build state-driven generative UI components

useFrontendTool

Complete reference for the useFrontendTool hook

useRenderActivityMessage

Reference for rendering agent activity