4d4bd19
CopilotKitDocs
  • Docs
  • Integrations
  • Reference
  • Free Developer Access
Get Started
QuickstartCoding Agents
Concepts
ArchitectureGenerative UI OverviewOSS vs Enterprise
Agentic Protocols
OverviewAG-UIAG-UI MiddlewareMCPA2A
Build Chat UIs
Prebuilt Components
CopilotChatCopilotSidebarCopilotPopup
Custom Look and Feel
CSS CustomizationSlots (Subcomponents)Fully Headless UIReasoning Messages
Multimodal AttachmentsVoice
Build Generative UI
Controlled
Tool-based Generative UITool RenderingState RenderingReasoning
Your Components
Display ComponentsInteractive Components
Declarative
A2UIDynamic Schema A2UIFixed Schema A2UI
Open-Ended
MCP Apps
Adding Agent Powers
Frontend ToolsShared State
Human-in-the-Loop
HITL OverviewPausing the Agent for InputHeadless Interrupts
Sub-AgentsAgent ConfigProgrammatic Control
Agents & Backends
Built-in Agent
Backend
Copilot RuntimeFactory ModeAG-UI
Runtime Server AdapterAuthentication
Built-in Agent (TanStack AI)
Advanced ConfigurationMCP ServersModel SelectionServer Tools
Observe & Operate
InspectorVS Code Extension
Troubleshooting
Common Copilot IssuesError Debugging & ObservabilityDebug ModeAG-UI Event InspectorHook ExplorerError Observability Connectors
Enterprise
CopilotKit PremiumHow the Enterprise Intelligence Platform WorksHow Threads & Persistence WorkObservabilitySelf-Hosting IntelligenceThreads
Deploy
AWS AgentCore
What's New
Full MCP Apps SupportLangGraph Deep Agents in CopilotKitA2UI Launches with full AG-UI SupportCopilotKit v1.50Generative UI Spec SupportA2A and MCP Handshake
Migrate
Migrate to V2Migrate to 1.8.2
Other
Contributing
Code ContributionsDocumentation Contributions
Anonymous Telemetry
Built-in Agent (TanStack AI)Premium FeaturesFully Headless UI

Fully Headless UI

Build chat interfaces from the ground up with the `useCopilotChatHeadless_c` hook — zero UI opinions, full generative UI + suggestions + interrupts support.

Overview#

CopilotKit offers fully headless UI through the useCopilotChatHeadless_c hook. By using this hook, you can build your own chat interfaces from the ground up while still utilizing CopilotKit's core features and ease-of-use.

Info

Fully Headless UI is an Early Access Premium feature. Grab a free publicLicenseKey at Copilot Cloud to unlock it.

Getting started#

Create a new application#

Scaffold a new CopilotKit project using the CLI:

npx copilotkit@latest create

Wire your CopilotKit provider with a license key#

Wrap your root layout with CopilotKit and pass in your public license key:

src/app/layout.tsx
tsx
<CopilotKit publicLicenseKey="your-free-public-license-key">
  {children}
</CopilotKit>

Build a headless chat component#

Use useCopilotChatHeadless_c to access messages, a send function, and loading state, then wire them to your own UI:

src/app/page.tsx
tsx
"use client";
import { useState } from "react";
import { useCopilotChatHeadless_c } from "@copilotkit/react-core/v2";

export default function Home() {
  const { messages, sendMessage, isLoading } = useCopilotChatHeadless_c();
  const [input, setInput] = useState("");

  const handleSend = () => {
    if (input.trim()) {
      sendMessage({
        id: Date.now().toString(),
        role: "user",
        content: input,
      });
      setInput("");
    }
  };

  return (
    <div>
      <h1>My Headless Chat</h1>

      <div>
        {messages.map((message) => (
          <div key={message.id}>
            <strong>{message.role === "user" ? "You" : "Assistant"}:</strong>
            <p>{message.content}</p>
          </div>
        ))}
        {isLoading && <p>Assistant is typing...</p>}
      </div>

      <div>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => e.key === "Enter" && handleSend()}
          placeholder="Type your message here..."
        />
        <button onClick={handleSend} disabled={isLoading}>
          Send
        </button>
      </div>
    </div>
  );
}

Working with generative UI#

You can render generative UI either via useFrontendTool / useComponent, or by reading tools and rendering them directly.

With useFrontendTool#

Register a frontend tool and attach a render function. CopilotKit inserts your component wherever that tool call appears in the message stream:

src/app/components/chat.tsx
tsx
import { useFrontendTool } from "@copilotkit/react-core/v2";

export const Chat = () => {
  const { messages } = useCopilotChatHeadless_c();

  useFrontendTool({
    name: "showCustomComponent",
    handler: () => "Foo, Bar, Baz",
    render: ({ result, args, status }) => (
      <div
        style={{
          backgroundColor: "red",
          padding: "10px",
          borderRadius: "5px",
        }}
      >
        <p>Custom component</p>
        <p>Result: {result}</p>
        <p>Args: {JSON.stringify(args)}</p>
        <p>Status: {status}</p>
      </div>
    ),
  });

  return (
    <div>
      {messages.map((message) => (
        <p key={message.id}>
          {message.role === "user" ? "User: " : "Assistant: "}
          {message.content}
          {message.role === "assistant" && message.generativeUI?.()}
        </p>
      ))}
    </div>
  );
};

With raw tool-call data#

If you don't want to use useFrontendTool, render the raw data directly:

src/app/components/chat.tsx
tsx
export const Chat = () => {
  const { messages } = useCopilotChatHeadless_c();

  return (
    <div>
      {messages.map((message) => (
        <p key={message.id}>
          {message.role === "assistant" &&
            message.toolCalls?.map((toolCall) => (
              <p key={toolCall.id}>
                {toolCall.function.name}: {toolCall.function.arguments}
              </p>
            ))}
        </p>
      ))}
    </div>
  );
};

Working with suggestions#

CopilotKit's suggestions give users a list of generated or static prompts. The headless hook exposes full control over the lifecycle.

Generating suggestions#

Use useCopilotChatSuggestions to generate and display prompt suggestions in your headless UI:

src/app/components/chat.tsx
tsx
import {
  useCopilotChatHeadless_c,
  useCopilotChatSuggestions,
} from "@copilotkit/react-core/v2";
import { useEffect } from "react";

export const Chat = () => {
  useCopilotChatSuggestions({
    instructions:
      "Suggest 5 interesting activities for programmers to do on their next vacation",
    maxSuggestions: 5,
  });

  const { suggestions, generateSuggestions, sendMessage } =
    useCopilotChatHeadless_c();

  useEffect(() => {
    generateSuggestions();
  }, []);

  return (
    <div>
      {suggestions.map((s, i) => (
        <button
          key={i}
          onClick={() =>
            sendMessage({
              id: Date.now().toString(),
              role: "user",
              content: s.message,
            })
          }
        >
          {s.title}
        </button>
      ))}
    </div>
  );
};

Programmatic suggestions#

If you want deterministic control, set suggestions manually:

const { suggestions, setSuggestions } = useCopilotChatHeadless_c();

useEffect(() => {
  setSuggestions([
    { title: "Suggestion 1", message: "The actual message for suggestion 1" },
    { title: "Suggestion 2", message: "The actual message for suggestion 2" },
  ]);
}, []);

Working with human-in-the-loop#

Human-in-the-loop (HITL) pauses the chat and waits for the user's input. It comes in two flavors: tool-based and interrupt-based (certain frameworks only).

Tool-based#

Tool-based HITL pauses tool execution until the user responds. The response becomes the tool's result.

src/app/components/chat.tsx
tsx
import {
  useFrontendTool,
  useCopilotChatHeadless_c,
} from "@copilotkit/react-core/v2";

export const Chat = () => {
  const { messages } = useCopilotChatHeadless_c();

  useFrontendTool({
    name: "getName",
    renderAndWaitForResponse: ({ respond, args, status }) => {
      if (status === "complete") return <div>Name retrieved…</div>;

      return (
        <div>
          <input
            value={args.name || ""}
            onChange={(e) => respond?.(e.target.value)}
            placeholder="Enter your name"
          />
          <button onClick={() => respond?.(args.name)}>Submit</button>
        </div>
      );
    },
  });

  return (
    <>
      {messages.map((message) => (
        <p key={message.id}>
          {message.role === "user" ? "User: " : "Assistant: "}
          {message.content}
          {message.role === "assistant" && message.generativeUI?.()}
        </p>
      ))}
    </>
  );
};
On this page
OverviewGetting startedCreate a new applicationWire your CopilotKit provider with a license keyBuild a headless chat componentWorking with generative UIWith useFrontendToolWith raw tool-call dataWorking with suggestionsGenerating suggestionsProgrammatic suggestionsWorking with human-in-the-loopTool-based