Slots
Customize any part of the chat UI by overriding individual sub-components via slots.
Open your coding agent in your project's folder, or in an empty folder for a new app.This runs in a coding agent on your computer.
What is this?#
Every CopilotKit chat component is built from composable slots — named sub-components that you can override individually. The slot system gives you three levels of customization without needing to rebuild the entire UI:
- Tailwind classes — pass a string to add/override CSS classes
- Props override — pass an object to override specific props on the default component
- Custom component — pass your own React component to fully replace a slot
Slots are recursive — you can drill into nested sub-components at any depth.
Tailwind Classes#
The simplest way to customize a slot. Pass a Tailwind class string and it will be merged with the default component's classes.
import { CopilotChat } from "@copilotkit/react-core/v2";
export function Chat() {
return (
<CopilotChat
messageView="bg-gray-50 dark:bg-gray-900 p-4"
input="border-2 border-blue-400 rounded-xl"
/>
);
}Props Override#
Pass an object to override specific props on the default component. This is useful for adding className, event handlers, data attributes, or any other prop the default component accepts.
<CopilotChat
messageView={{
className: "my-custom-messages",
"data-testid": "message-view",
}}
input={{ autoFocus: true }}
/>Custom Components#
For full control, pass your own React component. It receives all the same props as the default component.
import { CopilotChat } from "@copilotkit/react-core/v2";
const CustomMessageView = ({ messages, isRunning }) => (
<div className="space-y-4 p-6">
{messages?.map((msg) => (
<div
key={msg.id}
className={msg.role === "user" ? "text-right" : "text-left"}
>
{msg.content}
</div>
))}
{isRunning && <div className="animate-pulse">Thinking...</div>}
</div>
);
export function Chat() {
return (
<CopilotChat messageView={CustomMessageView} />
);
}Nested Slots (Drill-Down)#
Slots are recursive. You can customize sub-components at any depth by nesting objects.
Two levels deep#
Override the assistant message's toolbar within the message view:
<CopilotChat
messageView={{
assistantMessage: {
toolbar: CustomToolbar,
copyButton: CustomCopyButton,
},
userMessage: CustomUserMessage,
}}
/>Three levels deep#
Override a specific button inside the assistant message toolbar:
<CopilotChat
messageView={{
assistantMessage: {
copyButton: ({ onClick }) => <button onClick={onClick}>Copy</button>,
},
}}
/>Input sub-slots#
<CopilotChat
input={{
textArea: CustomTextArea,
sendButton: CustomSendButton,
}}
/>Scroll view sub-slots#
<CopilotChat
scrollView={{
feather: CustomFeather,
scrollToBottomButton: CustomScrollButton,
}}
/>Suggestion view sub-slots#
<CopilotChat
suggestionView={{
suggestion: CustomSuggestionPill,
container: CustomSuggestionContainer,
}}
/>Children Render Function#
For complete layout control, use the children render function pattern. This gives you pre-built slot elements that you can arrange however you want.
import { CopilotChat } from "@copilotkit/react-core/v2";
export function Chat() {
return (
<CopilotChat>
{({ messageView, input, scrollView, suggestionView }) => (
<div className="flex flex-col h-full">
<header className="p-4 border-b font-semibold">My Agent</header>
{scrollView}
<div className="border-t p-4">{input}</div>
</div>
)}
</CopilotChat>
);
}Reshaping the Message List#
Slots change how each message renders. To change which messages render — hide some, replace them, reorder them — pass transformMessages to the message view. It receives the whole list and returns the list to render.
import { useCallback } from "react";
import { CopilotChat, type Message } from "@copilotkit/react-core/v2";
export function Chat() {
const transformMessages = useCallback(
(messages: Message[]) => messages.filter((m) => m.role !== "tool"),
[],
);
return <CopilotChat messageView={{ transformMessages }} />;
}A few things to know:
- Long threads stay virtualized. Once a thread passes 50 messages the chat only mounts the rows on screen. It counts the messages
transformMessagesreturns, so hidden messages take no row. - Tool cards keep their results. Tool-call renderers look up results in the full list, so hiding tool-result messages, as above, does not empty the cards that display them.
- Keep ids unique and stable. Return each message id at most once. A message you build yourself, such as one that stands in for several, needs its own id, and that id must stay the same while the thread streams. Derive it from something fixed, like the id of the first message it replaces. The chat uses the last message you return to decide which one is still streaming.
- Pass a stable function. The transform reruns whenever the messages or the function change. An inline function works, but reruns on every render.
If you pass the message view a children render function to lay out the rendered messages yourself, virtualization turns off and every message is mounted. Prefer transformMessages for list-level changes; in development the chat warns when children is what turned virtualization off.
Labels#
Customize any text string in the UI via the labels prop. This does not use the slot system — it's a separate convenience prop on CopilotChat, CopilotSidebar, and CopilotPopup.
<CopilotChat
labels={{
chatInputPlaceholder: "Ask your agent anything...",
welcomeMessageText: "How can I help you today?",
chatDisclaimerText: "AI responses may be inaccurate.",
}}
/>Available Slots#
CopilotChat / CopilotSidebar / CopilotPopup#
These are the root-level slot props available on all chat components:
| Slot | Description |
|---|---|
messageView | The message list container. |
scrollView | The scroll container with auto-scroll behavior. |
input | The text input area with send/transcribe controls. |
suggestionView | The suggestion pills shown below messages. |
welcomeScreen | The initial empty-state screen (pass false to disable). |
CopilotSidebar and CopilotPopup also have:
| Slot | Description |
|---|---|
header | The modal header bar. |
toggleButton | The open/close toggle button. |
messageView sub-slots#
Available via messageView={{ ... }}:
| Slot | Description |
|---|---|
assistantMessage | Renders assistant responses. Has its own sub-slots (see below). |
userMessage | Renders user messages. Has its own sub-slots (see below). |
reasoningMessage | Renders model reasoning/thinking steps. Has its own sub-slots (see below). |
cursor | The streaming cursor indicator shown while the agent is responding. |
assistantMessage sub-slots#
Available via messageView={{ assistantMessage: { ... } }}:
| Slot | Description |
|---|---|
markdownRenderer | The markdown rendering component. See Markdown Rendering. |
toolbar | The action toolbar below messages. |
copyButton | Copy message button. |
thumbsUpButton | Thumbs up feedback button. |
thumbsDownButton | Thumbs down feedback button. |
readAloudButton | Read aloud button. |
regenerateButton | Regenerate response button. |
toolCallsView | Tool call visualization. |
userMessage sub-slots#
Available via messageView={{ userMessage: { ... } }}:
| Slot | Description |
|---|---|
messageRenderer | The text rendering component for user messages. |
toolbar | The action toolbar on hover. |
copyButton | Copy message button. |
editButton | Edit message button. |
branchNavigation | Navigation between message branches (after editing). |
reasoningMessage sub-slots#
Available via messageView={{ reasoningMessage: { ... } }}:
| Slot | Description |
|---|---|
header | The collapsible header (click to expand/collapse). |
contentView | The reasoning content area. |
toggle | The expand/collapse toggle wrapper. |
input sub-slots#
Available via input={{ ... }}:
| Slot | Description |
|---|---|
textArea | The text input element. |
sendButton | The send/submit button. |
addMenuButton | The attachment/tools menu button. |
startTranscribeButton | Button to start voice transcription. |
cancelTranscribeButton | Button to cancel transcription. |
finishTranscribeButton | Button to finish transcription. |
audioRecorder | The audio recorder component. |
disclaimer | The disclaimer text below the input. |
scrollView sub-slots#
Available via scrollView={{ ... }}:
| Slot | Description |
|---|---|
feather | The gradient overlay at the bottom of the scroll area. |
scrollToBottomButton | The button that appears when scrolled up. |
suggestionView sub-slots#
Available via suggestionView={{ ... }}:
| Slot | Description |
|---|---|
suggestion | Individual suggestion pill/button. |
container | The container wrapping all suggestion pills. |
welcomeScreen sub-slots#
Available via welcomeScreen={{ ... }}:
| Slot | Description |
|---|---|
welcomeMessage | The welcome text shown on the empty state. |
header sub-slots (Sidebar/Popup only)#
Available via header={{ ... }}:
| Slot | Description |
|---|---|
titleContent | The title text in the header. |
closeButton | The close/minimize button. |
toggleButton sub-slots (Sidebar/Popup only)#
Available via toggleButton={{ ... }}:
| Slot | Description |
|---|---|
openIcon | Icon shown when the chat is closed. |
closeIcon | Icon shown when the chat is open. |