showcase/shell-docs/src/content/snippets/shared/basics/headless-ui.mdx
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.
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.
Use useAgent to get the agent instance (messages, state, execution status) and useCopilotKit to run the agent.
import { useAgent } from "@copilotkit/react-core/v2";
import { useCopilotKit } from "@copilotkit/react-core/v2";
import { randomUUID } from "@copilotkit/shared/v2";
export function CustomChat() {
// [!code highlight:2]
const { agent } = useAgent();
const { copilotkit } = useCopilotKit();
return <div></div>;
}
The agent's messages are available via agent.messages. Each message has an id, role ("user" or "assistant"), and content.
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>
);
}
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.
import { useState, useCallback } from "react";
export function CustomChat() {
const { agent } = useAgent();
const { copilotkit } = useCopilotKit();
const [input, setInput] = useState("");
// [!code highlight:14]
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>
);
}
Use copilotkit.stopAgent() to cancel a running agent:
const stopAgent = useCallback(() => {
// [!code highlight:1]
copilotkit.stopAgent({ agent });
}, [agent, copilotkit]);
// In your JSX:
{
agent.isRunning && (
<button onClick={stopAgent} className="text-red-500">
Stop
</button>
);
}