Open, close, and feedback

Open and close the prebuilt chat from your UI, and capture assistant message feedback.

Two common controls for the prebuilt chat are opening it from your own UI and capturing thumbs-up / thumbs-down feedback on assistant messages.

Control the open state from your own UI#

Pass open and onOpenChange to <CopilotSidebar> or <CopilotPopup> to own the open state yourself. This is the controlled pattern: the surface renders whatever open says, and every request to open or close (the toggle button, click-outside on the popup) arrives on onOpenChange instead of moving the surface directly.

import { useState } from "react";
import { CopilotSidebar } from "@copilotkit/react-core/v2";

function Layout() {
  const [chatOpen, setChatOpen] = useState(false);

  return (
    <>
      <nav>
        <button onClick={() => setChatOpen(true)}>Ask the assistant</button>
      </nav>

      <CopilotSidebar open={chatOpen} onOpenChange={setChatOpen} />
    </>
  );
}

The button lives outside the sidebar, which is the point: nothing has to be rendered inside the chat subtree to open or close it. Because open is the source of truth, you can also keep the chat in sync with a route, a keyboard shortcut, or any other state you already have.

onOpenChange reports a request, not a change. If you do not feed the new value back into open, the surface stays where it is. That is what makes it possible to gate opening the chat, for example behind a sign-in check. You can also pass onOpenChange on its own, without open: the surface keeps managing itself and you are notified each time it opens or closes.

Programmatically open or close the chat#

If you would rather not lift the state into your own component, the open state for <CopilotPopup> and <CopilotSidebar> also lives in the chat configuration context. Read it with useCopilotChatConfiguration() and call setModalOpen(true | false) from anywhere inside the provider:

import { useCopilotChatConfiguration } from "@copilotkit/react-core/v2";

function OpenChatButton() {
  const config = useCopilotChatConfiguration();

  // setModalOpen is only present when a provider in the tree owns modal state
  // (the prebuilt CopilotPopup / CopilotSidebar create it for you).
  if (!config?.setModalOpen) return null;

  return (
    <button onClick={() => config.setModalOpen(true)}>
      Ask the assistant
    </button>
  );
}

To toggle, read config.isModalOpen and flip it:

<button onClick={() => config.setModalOpen(!config.isModalOpen)}>
  {config.isModalOpen ? "Close chat" : "Open chat"}
</button>

setModalOpen / isModalOpen are only defined when a provider in the tree owns modal state. The prebuilt <CopilotPopup> and <CopilotSidebar> create it automatically. If you compose chat yourself, wrap the relevant subtree in <CopilotChatConfigurationProvider isModalDefaultOpen={false}> so the modal state exists. See the useCopilotChatConfiguration reference.

You can also set the initial open state declaratively, and then let the surface manage itself. The prebuilt surfaces accept a defaultOpen prop:

<CopilotSidebar defaultOpen={false} />

defaultOpen and open are the usual uncontrolled/controlled pair: use defaultOpen for a starting point, and open when your own state decides. When both are passed, open wins.

Capture message feedback (thumbs up / down)#

The assistant-message toolbar can show thumbs-up and thumbs-down buttons. In v2 you opt in by passing onThumbsUp / onThumbsDown to the assistant message slot. The buttons only render when a handler is provided:

import { CopilotChat } from "@copilotkit/react-core/v2";

<CopilotChat
  messageView={{
    assistantMessage: {
      onThumbsUp: (message) => {
        analytics.track("feedback", { messageId: message.id, value: "up" });
      },
      onThumbsDown: (message) => {
        analytics.track("feedback", { messageId: message.id, value: "down" });
      },
    },
  }}
/>;

Each handler receives the assistant message, so you can record the feedback against the specific response (message.id). The same messageView slot works on <CopilotPopup> and <CopilotSidebar> since they wrap <CopilotChat>.

When the slot is rendered through CopilotChatMessageView, a live assistant message created by a direct AG-UI TEXT_MESSAGE_START can also include that event's opaque rawEvent value. The join happens when the thumbs callback runs; canonical messages and future run input stay unchanged. Chunk, snapshot, persisted, legacy, and direct CopilotChatAssistantMessage paths don't provide this callback metadata.

The button labels come from the chat labels (assistantMessageToolbarThumbsUpLabel defaults to "Good response", assistantMessageToolbarThumbsDownLabel to "Bad response"); override them via chat labels.

Building a fully custom message component instead of using the slot? The underlying CopilotChatAssistantMessage exposes the same onThumbsUp / onThumbsDown props directly, but callback metadata enrichment is owned by CopilotChatMessageView.