CopilotKit

Fully Headless UI

Fully customize your Copilot's UI from the ground up using headless UI


What is this?#

A headless UI gives you full control over the chat experience — you bring your own components, layout, and styling while CopilotKit handles agent communication, message management, and streaming. This is built on top of the same primitives (useAgent and useCopilotKit) covered in Programmatic Control.

When should I use this?#

Use headless UI when the slot system isn't enough — for example, when you need a completely different layout, want to embed the chat into an existing UI, or are building a non-chat interface that still communicates with an agent.

Implementation#

Access the agent and CopilotKit#

Use useAgent to get the agent instance (messages, state, execution status) and useCopilotKit to run the agent.

components/custom-chat.tsx
import { useAgent } from "@copilotkit/react-core/v2";
import { useCopilotKit } from "@copilotkit/react-core/v2";
import { randomUUID } from "@copilotkit/shared/v2";

export function CustomChat() {
  const { agent } = useAgent();
  const { copilotkit } = useCopilotKit();

  return <div>{/* Your custom UI */}</div>;
}

Display messages#

The agent's messages are available via agent.messages. Each message has an id, role ("user" or "assistant"), and content.

components/custom-chat.tsx
export function CustomChat() {
  const { agent } = useAgent();
  const { copilotkit } = useCopilotKit();

  return (
    <div className="flex flex-col h-full">
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {agent.messages.map((msg) => (
          <div
            key={msg.id}
            className={
              msg.role === "user"
                ? "ml-auto bg-blue-100 rounded-lg p-3 max-w-md"
                : "bg-gray-100 rounded-lg p-3 max-w-md"
            }
          >
            <p className="text-sm font-medium">{msg.role}</p>
            <p>{msg.content}</p>
          </div>
        ))}
        {agent.isRunning && <div className="text-gray-400">Thinking...</div>}
      </div>
    </div>
  );
}

Send messages and run the agent#

Add a message to the agent's conversation, then call copilotkit.runAgent() to trigger execution. This is the same method CopilotKit's built-in <CopilotChat /> uses internally.

components/custom-chat.tsx
import { useState, useCallback } from "react";

export function CustomChat() {
  const { agent } = useAgent();
  const { copilotkit } = useCopilotKit();
  const [input, setInput] = useState("");

  const sendMessage = useCallback(async () => {
    if (!input.trim()) return;

    agent.addMessage({
      id: randomUUID(),
      role: "user",
      content: input,
    });

    setInput("");

    await copilotkit.runAgent({ agent });
  }, [input, agent, copilotkit]);

  return (
    <div className="flex flex-col h-full">
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {agent.messages.map((msg) => (
          <div
            key={msg.id}
            className={
              msg.role === "user"
                ? "ml-auto bg-blue-100 rounded-lg p-3 max-w-md"
                : "bg-gray-100 rounded-lg p-3 max-w-md"
            }
          >
            <p>{msg.content}</p>
          </div>
        ))}
        {agent.isRunning && <div className="text-gray-400">Thinking...</div>}
      </div>

      <form
        className="border-t p-4 flex gap-2"
        onSubmit={(e) => {
          e.preventDefault();
          sendMessage();
        }}
      >
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Type a message..."
          className="flex-1 border rounded-lg px-3 py-2"
        />
        <button type="submit" disabled={agent.isRunning}>
          Send
        </button>
      </form>
    </div>
  );
}

Stop the agent#

Use copilotkit.stopAgent() to cancel a running agent:

components/custom-chat.tsx
const stopAgent = useCallback(() => {
  copilotkit.stopAgent({ agent });
}, [agent, copilotkit]);

// In your JSX:
{
  agent.isRunning && (
    <button onClick={stopAgent} className="text-red-500">
      Stop
    </button>
  );
}

Human-in-the-Loop with Headless UI#

For human-in-the-loop interactions with custom UI, use useHumanInTheLoop to create approval workflows:

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

  useHumanInTheLoop({
    name: "approvalRequired",
    description: "Request user approval for an operation",
    parameters: [
      {
        name: "operation",
        type: "string",
        description: "The operation to approve",
        required: true,
      },
    ],
    render: ({ args, respond }) => {
      if (!respond) return null;

      return (
        <div>
          <p>Approval Required</p>
          <p>Operation: {args.operation}</p>
          <button onClick={() => respond("APPROVED")}>Approve</button>
          <button onClick={() => respond("REJECTED")}>Reject</button>
        </div>
      );
    },
  });

  return <div>{/* Your custom chat UI */}</div>;
};

See Human-in-the-Loop for more details on approval workflows.