Fully Headless UI
Premium
Fully customize your Copilot's UI from the ground up using headless UI
Early Access feature
The Fully Headless UI extension is in Early Access to CopilotKit Premium users. As it matures, it will become available as part of the core framework.
- Get started with your public license key from Copilot Cloud
- Or read more about Premium Features.
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.
Navigation
This page has all the information you need to get started with CopilotKit's headless UI. Select where you'd like to get started below.
Getting started
Build a fully headless chat interface from the ground up to get started.
Generative UI
Learn how to work with generative UI to render tools and other UI elements.
Suggestions
Learn how to work with suggestions to provide your users with a list of generated or static options to operate the chat with.
Human-in-the-loop
Learn how to work with human-in-the-loop to pause the chat and wait for a human to respond.
Reference
Reference documentation for the `useCopilotChatHeadless_c` hook.
Getting started
To get started, let's walk through building a simple chat interface. From there, we'll cover how to do more advanced things like working with suggestions and generative UI.
To get there, let's start by building a simple chat interface.
Create a new application
npx copilotkit@latest createSet up your application
First, follow the instructions in the README to set up your application, it will be a simple process.
open README.mdSet up your CopilotKit provider
You will need to provide your public license key to the CopilotKit provider component. Get yours on Copilot Cloud or read more about premium features.
<CopilotKit
publicLicenseKey="your-free-public-license-key"
>
{children}
</CopilotKit>Create a headless chat component
Replace your main page with a simple chat interface using the headless hook.
"use client";
import { useState } from "react";
import { useCopilotChatHeadless_c } from "@copilotkit/react-core";
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>
{/* Messages */}
<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>
{/* Input */}
<div>
<input
type="text"
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 also render generative UI either via useCopilotAction or by reading tools and rendering them directly.
With useCopilotAction
CopilotKit's standard components utilize useCopilotAction to allow tools to be both rendered and called in
the frontend. These same interfaces are available to you while using the useCopilotChatHeadless_c hook.
import { useCopilotAction } from "@copilotkit/react-core";
export const Chat = () => {
// ...
// Define an action that will show a custom component
useCopilotAction({
name: "showCustomComponent",
// Handle the tool on the frontend
handler: () => {
return "Foo, Bar, Baz";
},
// Render a custom component for the underlying data
render: ({ result, args, status}) => {
return <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}
{/* Render the generative UI if it exists */}
{message.role === "assistant" && message.generativeUI?.()}
</p>
))}
</div>
};With raw data
If you don't want to use useCopilotAction, you can also render the raw data directly.
export const Chat = () => {
// ...
return <div>
{messages.map((message) => (
<p key={message.id}>
{/* Render the tool calls if they exist */}
{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 are a way to provide your users with a list of suggestions to operate the chat with. When utilizing Headless UI, you have full control over the lifecycle of these suggestions.
Generating suggestions
You can generate suggestions by calling the generateSuggestions function.
import { useCopilotChatHeadless_c, useCopilotChatSuggestions } from "@copilotkit/react-core";
export const Chat = () => {
// Specify what suggestions should be generated
useCopilotChatSuggestions({
instructions:
"Suggest 5 interesting activities for programmers to do on their next vacation",
maxSuggestions: 5,
});
// Grab relevant state from the headless hook
const { suggestions, generateSuggestions, sendMessage } = useCopilotChatHeadless_c();
// Generate suggestions when the component mounts
useEffect(() => {
generateSuggestions();
}, []);
// ...
return <div>
{suggestions.map((suggestion, index) => (
<button
key={index}
onClick={() => sendMessage({
id: "123",
role: "user",
content: suggestion.message
})}
>
{suggestion.title}
</button>
))}
</div>
};Programmatically setting suggestions
If you want more deterministic control over the suggestions, you can set them manually.
import { useCopilotChatHeadless_c } from "@copilotkit/react-core";
export const Chat = () => {
// Grab relevant state from the headless hook
const { suggestions, setSuggestions } = useCopilotChatHeadless_c();
// Set the suggestions when the component mounts
useEffect(() => {
setSuggestions([
{ title: "Suggestion 1", message: "The actual message for suggestion 1" },
{ title: "Suggestion 2", message: "The actual message for suggestion 2" },
]);
}, []);
// Change the suggestions on function call
const changeSuggestions = () => {
setSuggestions([
{ title: "Foo", message: "Bar" },
{ title: "Baz", message: "Bat" },
]);
};
return (
<div>
{/* Change on button click */}
<button onClick={changeSuggestions}>Change suggestions</button>
{/* Render */}
{suggestions.map((suggestion, index) => (
<button
key={index}
onClick={() => sendMessage({
id: "123",
role: "user",
content: suggestion.message
})}
>
{suggestion.title}
</button>
))}
</div>
);
};Working with Human-in-the-loop
CopilotKit's human-in-the-loop (HITL) features allows you to pause the chat and wait for a human to respond. This comes in two flavors: tool-based and interrupt-based (for certain frameworks).
Tool-based
Tool-based HITL allows for you to pause completion of a tool call and wait for a human to respond. That human's response, becomes the result of the tool call.
import { useCopilotAction, useCopilotChatHeadless_c } from "@copilotkit/react-core";
export const Chat = () => {
const { messages, sendMessage } = useCopilotChatHeadless_c();
// Define an action that will wait for the user to enter their name
useCopilotAction({
name: "getName",
renderAndWaitForResponse: ({ respond, args, status}) => {
if (status === "complete") {
return <div>
<p>Name retrieved...</p>
</div>;
}
return <div>
<input
type="text"
value={args.name || ""}
onChange={(e) => respond?.(e.target.value)}
placeholder="Enter your name"
/>
{/* Respond with the name */}
<button onClick={() => respond?.(args.name)}>Submit</button>
</div>;
}
});
return (
{messages.map((message) => (
<p key={message.id}>
{message.role === "user" ? "User: " : "Assistant: "}
{message.content}
{/* This will render the tool-based HITL if it exists */}
{message.role === "assistant" && message.generativeUI?.()}
</p>
))}
)
};