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:

  1. Tailwind classes — pass a string to add/override CSS classes
  2. Props override — pass an object to override specific props on the default component
  3. 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.

page.tsx
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.

page.tsx
<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.

page.tsx
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:

page.tsx
<CopilotChat
  messageView={{
    assistantMessage: {
      toolbar: CustomToolbar,
      copyButton: CustomCopyButton,
    },
    userMessage: CustomUserMessage,
  }}
/>

Three levels deep#

Override a specific button inside the assistant message toolbar:

page.tsx
<CopilotChat
  messageView={{
    assistantMessage: {
      copyButton: ({ onClick }) => <button onClick={onClick}>Copy</button>,
    },
  }}
/>

Input sub-slots#

page.tsx
<CopilotChat
  input={{
    textArea: CustomTextArea,
    sendButton: CustomSendButton,
  }}
/>

Scroll view sub-slots#

page.tsx
<CopilotChat
  scrollView={{
    feather: CustomFeather,
    scrollToBottomButton: CustomScrollButton,
  }}
/>

Suggestion view sub-slots#

page.tsx
<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.

page.tsx
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.

page.tsx
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 transformMessages returns, 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.

page.tsx
<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:

SlotDescription
messageViewThe message list container.
scrollViewThe scroll container with auto-scroll behavior.
inputThe text input area with send/transcribe controls.
suggestionViewThe suggestion pills shown below messages.
welcomeScreenThe initial empty-state screen (pass false to disable).

CopilotSidebar and CopilotPopup also have:

SlotDescription
headerThe modal header bar.
toggleButtonThe open/close toggle button.

messageView sub-slots#

Available via messageView={{ ... }}:

SlotDescription
assistantMessageRenders assistant responses. Has its own sub-slots (see below).
userMessageRenders user messages. Has its own sub-slots (see below).
reasoningMessageRenders model reasoning/thinking steps. Has its own sub-slots (see below).
cursorThe streaming cursor indicator shown while the agent is responding.

assistantMessage sub-slots#

Available via messageView={{ assistantMessage: { ... } }}:

SlotDescription
markdownRendererThe markdown rendering component. See Markdown Rendering.
toolbarThe action toolbar below messages.
copyButtonCopy message button.
thumbsUpButtonThumbs up feedback button.
thumbsDownButtonThumbs down feedback button.
readAloudButtonRead aloud button.
regenerateButtonRegenerate response button.
toolCallsViewTool call visualization.

userMessage sub-slots#

Available via messageView={{ userMessage: { ... } }}:

SlotDescription
messageRendererThe text rendering component for user messages.
toolbarThe action toolbar on hover.
copyButtonCopy message button.
editButtonEdit message button.
branchNavigationNavigation between message branches (after editing).

reasoningMessage sub-slots#

Available via messageView={{ reasoningMessage: { ... } }}:

SlotDescription
headerThe collapsible header (click to expand/collapse).
contentViewThe reasoning content area.
toggleThe expand/collapse toggle wrapper.

input sub-slots#

Available via input={{ ... }}:

SlotDescription
textAreaThe text input element.
sendButtonThe send/submit button.
addMenuButtonThe attachment/tools menu button.
startTranscribeButtonButton to start voice transcription.
cancelTranscribeButtonButton to cancel transcription.
finishTranscribeButtonButton to finish transcription.
audioRecorderThe audio recorder component.
disclaimerThe disclaimer text below the input.

scrollView sub-slots#

Available via scrollView={{ ... }}:

SlotDescription
featherThe gradient overlay at the bottom of the scroll area.
scrollToBottomButtonThe button that appears when scrolled up.

suggestionView sub-slots#

Available via suggestionView={{ ... }}:

SlotDescription
suggestionIndividual suggestion pill/button.
containerThe container wrapping all suggestion pills.

welcomeScreen sub-slots#

Available via welcomeScreen={{ ... }}:

SlotDescription
welcomeMessageThe welcome text shown on the empty state.

header sub-slots (Sidebar/Popup only)#

Available via header={{ ... }}:

SlotDescription
titleContentThe title text in the header.
closeButtonThe close/minimize button.

toggleButton sub-slots (Sidebar/Popup only)#

Available via toggleButton={{ ... }}:

SlotDescription
openIconIcon shown when the chat is closed.
closeIconIcon shown when the chat is open.