Back to Copilotkit

Bring Your Own Components

showcase/shell-docs/src/content/snippets/shared/guides/custom-look-and-feel/bring-your-own-components.mdx

1.57.017.1 KB
Original Source

import { ImageAndCode } from "@/components/react/image-and-code";

You can swap out any of the sub-components of any Copilot UI to build up a completely custom look and feel. All components are fully typed with TypeScript for better development experience.

ComponentDescription
UserMessageMessage component for user messages
AssistantMessageMessage component for assistant messages
WindowContains the chat
ButtonButton that opens/closes the chat
HeaderThe header of the chat
MessagesThe chat messages area
SuggestionsCustomize how suggestions are displayed
InputThe chat input
ActionsCustomize how actions (tools) are displayed
Agent StateCustomize how agent state messages are displayed
Reasoning MessageCustomize how model reasoning/thinking is displayed

UserMessage

The user message is what displays when the user sends a message to the chat. In this example, we change the color and add an avatar.

<ImageAndCode preview="https://cdn.copilotkit.ai/docs/copilotkit/images/custom-user-message.png"> The main thing to be aware of here is the `message` prop, which is the message text from the user.
tsx
import { type UserMessageProps } from "@copilotkit/react-ui";
import { CopilotKit } from "@copilotkit/react-core";
import { CopilotSidebar } from "@copilotkit/react-ui";
import "@copilotkit/react-ui/v2/styles.css";

const CustomUserMessage = (props: UserMessageProps) => {
  const wrapperStyles = "flex items-center gap-2 justify-end mb-4";
  const messageStyles =
    "bg-blue-500 text-white py-2 px-4 rounded-xl break-words flex-shrink-0 max-w-[80%]";
  const avatarStyles =
    "bg-blue-500 shadow-sm min-h-10 min-w-10 rounded-full text-white flex items-center justify-center";

  return (
    <div className={wrapperStyles}>
      <div className={messageStyles}>{props.message?.content}</div>
      <div className={avatarStyles}>TS</div>
    </div>
  );
};

<CopilotKit>
  <CopilotSidebar UserMessage={CustomUserMessage} />
</CopilotKit>;
</ImageAndCode>

AssistantMessage

The assistant message is what displays when the LLM responds to a user message. In this example, we remove the background color and add an avatar.

<ImageAndCode preview="https://cdn.copilotkit.ai/docs/copilotkit/images/custom-assistant-message.png"> ```tsx import { type AssistantMessageProps } from "@copilotkit/react-ui"; import { useChatContext } from "@copilotkit/react-ui"; import { Markdown } from "@copilotkit/react-ui"; import { SparklesIcon } from "@heroicons/react/24/outline";

import { CopilotKit } from "@copilotkit/react-core"; import { CopilotSidebar } from "@copilotkit/react-ui"; import "@copilotkit/react-ui/v2/styles.css";

const CustomAssistantMessage = (props: AssistantMessageProps) => { const { icons } = useChatContext(); const { message, isLoading, subComponent } = props;

const avatarStyles = "bg-zinc-400 border-zinc-500 shadow-lg min-h-10 min-w-10 rounded-full text-white flex items-center justify-center"; const messageStyles = "px-4 rounded-xl pt-2";

const avatar = <div className={avatarStyles}><SparklesIcon className="h-6 w-6" /></div>

// [!code highlight:12] return (

<div className="py-2"> <div className="flex items-start"> {!subComponent && avatar} <div className={messageStyles}> {message && <Markdown content={message.content || ""} /> } {isLoading && icons.spinnerIcon} </div> </div> <div className="my-2">{subComponent}</div> </div> ); }; <CopilotKit> <CopilotSidebar AssistantMessage={CustomAssistantMessage} /> </CopilotKit> ``` **Key concepts** - `subComponent` - This is where any generative UI will be rendered. - `message` - This is the message text from the LLM, typically in markdown format. - `isLoading` - This is a boolean that indicates if the message is still loading. </ImageAndCode>

Window

The window is the main container for the chat. In this example, we turn it into a more traditional modal.

<ImageAndCode preview="https://cdn.copilotkit.ai/docs/copilotkit/images/custom-window.png">
tsx
import {
  type WindowProps,
  useChatContext,
  CopilotSidebar,
} from "@copilotkit/react-ui";
import { CopilotKit } from "@copilotkit/react-core";
import "@copilotkit/react-ui/v2/styles.css";
function Window({ children }: WindowProps) {
  const { open, setOpen } = useChatContext();

  if (!open) return null;

  // [!code highlight:15]
  return (
    <div
      className="fixed inset-0 bg-black/50 flex items-center justify-center p-4"
      onClick={() => setOpen(false)}
    >
      <div
        className="bg-white rounded-lg shadow-xl max-w-2xl w-full h-[80vh] overflow-auto"
        onClick={(e) => e.stopPropagation()}
      >
        <div className="flex flex-col h-full">{children}</div>
      </div>
    </div>
  );
}

<CopilotKit>
  <CopilotSidebar Window={Window} />
</CopilotKit>;
</ImageAndCode>

Button

The CopilotSidebar and CopilotPopup components allow you to customize their trigger button by passing in a custom Button component.

<ImageAndCode preview="https://cdn.copilotkit.ai/docs/copilotkit/images/custom-button.png">
tsx
import {
  type ButtonProps,
  useChatContext,
  CopilotSidebar,
} from "@copilotkit/react-ui";
import { CopilotKit } from "@copilotkit/react-core";
import "@copilotkit/react-ui/v2/styles.css";
function Button({}: ButtonProps) {
  const { open, setOpen } = useChatContext();

  const wrapperStyles =
    "w-24 bg-blue-500 text-white p-4 rounded-lg text-center cursor-pointer";

  // [!code highlight:10]
  return (
    <div onClick={() => setOpen(!open)} className={wrapperStyles}>
      <button
        className={`${open ? "open" : ""}`}
        aria-label={open ? "Close Chat" : "Open Chat"}
      >
        Ask AI
      </button>
    </div>
  );
}

<CopilotKit>
  <CopilotSidebar Button={Button} />
</CopilotKit>;
</ImageAndCode>

The header component is the top of the chat window. In this example, we add a button to the left of the title with a custom icon.

<ImageAndCode preview="https://cdn.copilotkit.ai/docs/copilotkit/images/custom-header.png">
tsx
import {
  type HeaderProps,
  useChatContext,
  CopilotSidebar,
} from "@copilotkit/react-ui";
import { BookOpenIcon } from "@heroicons/react/24/outline";
import { CopilotKit } from "@copilotkit/react-core";
import "@copilotkit/react-ui/v2/styles.css";
function Header({}: HeaderProps) {
  const { setOpen, icons, labels } = useChatContext();

  // [!code highlight:15]
  return (
    <div className="flex justify-between items-center p-4 bg-blue-500 text-white">
      <div className="w-24">
        <a href="/">
          <BookOpenIcon className="w-6 h-6" />
        </a>
      </div>
      <div className="text-lg">{labels.modalHeaderTitle}</div>
      <div className="w-24 flex justify-end">
        <button onClick={() => setOpen(false)} aria-label="Close">
          {icons.headerCloseIcon}
        </button>
      </div>
    </div>
  );
}

<CopilotKit>
  <CopilotSidebar Header={Header} />
</CopilotKit>;
</ImageAndCode>

Messages

The Messages component handles the display and organization of different message types in the chat interface. Its complexity comes from managing various message types (text, actions, results, and agent states) and maintaining proper scroll behavior.

<ImageAndCode preview="https://cdn.copilotkit.ai/docs/copilotkit/images/custom-messages.png">
tsx
import { type MessagesProps, CopilotSidebar } from "@copilotkit/react-ui";
import { CopilotKit } from "@copilotkit/react-core";
import "@copilotkit/react-ui/v2/styles.css";

export default function CustomMessages({
  messages,
  inProgress,
  RenderMessage,
}: MessagesProps) {
  const wrapperStyles =
    "p-4 flex flex-col gap-2 h-full overflow-y-auto bg-indigo-300";

  // [!code highlight:14]
  return (
    <div className={wrapperStyles}>
      {messages.map((message, index) => {
        const isCurrentMessage = index === messages.length - 1;
        return (
          <RenderMessage
            key={index}
            message={message}
            inProgress={inProgress}
            index={index}
            isCurrentMessage={isCurrentMessage}
          />
        );
      })}
    </div>
  );
}

<CopilotKit>
  <CopilotSidebar Messages={CustomMessages} />
</CopilotKit>;
</ImageAndCode>

Suggestions

The suggestions component allows you to customize how suggestions are displayed. In this example, we add a label to the list and change the suggestion chip look

<ImageAndCode preview="https://cdn.copilotkit.ai/docs/copilotkit/images/custom-suggestions-list.png">
The main props to be aware of are the `suggestions` array and `onSuggestionClick` function.

```tsx
    import { CopilotKit } from "@copilotkit/react-core";
    import {
        CopilotSidebar,
        type CopilotChatSuggestion,
        RenderSuggestion,
        type RenderSuggestionsListProps
    } from "@copilotkit/react-ui";
    import "@copilotkit/react-ui/v2/styles.css";

    const CustomSuggestionsList = ({ suggestions, onSuggestionClick }: RenderSuggestionsListProps) => {
        return (
            <div className="suggestions flex flex-col gap-2 p-4">
                <h1>Try asking:</h1>
                <div className="flex gap-2">
                    {suggestions.map((suggestion: CopilotChatSuggestion, index) => (
                        <RenderSuggestion
                        key={index}
                                  title={suggestion.title}
                                  message={suggestion.message}
                                  partial={suggestion.partial}
                                  className="rounded-md border border-gray-500 bg-white px-2 py-1 shadow-md"
                                  onClick={() => onSuggestionClick(suggestion.message)}
                        />
                    ))}
                </div>
            </div>
        );
    };

    <CopilotKit>
        <CopilotSidebar RenderSuggestionsList={CustomSuggestionsList} />
    </CopilotKit>
```
</ImageAndCode>

Input

The input component that the user interacts with to send messages to the chat. In this example, we customize it to have a custom "Ask" button and placeholder text.

<ImageAndCode preview="https://cdn.copilotkit.ai/docs/copilotkit/images/custom-input.png">
tsx
import { type InputProps, CopilotSidebar } from "@copilotkit/react-ui";
import { CopilotKit } from "@copilotkit/react-core";
import "@copilotkit/react-ui/v2/styles.css";
function CustomInput({ inProgress, onSend, isVisible }: InputProps) {
  const handleSubmit = (value: string) => {
    if (value.trim()) onSend(value);
  };

  const wrapperStyle = "flex gap-2 p-4 border-t";
  const inputStyle =
    "flex-1 p-2 rounded-md border border-gray-300 focus:outline-none focus:border-blue-500 disabled:bg-gray-100";
  const buttonStyle =
    "px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed";

  // [!code highlight:27]
  return (
    <div className={wrapperStyle}>
      <input
        disabled={inProgress}
        type="text"
        placeholder="Ask your question here..."
        className={inputStyle}
        onKeyDown={(e) => {
          if (e.key === "Enter") {
            handleSubmit(e.currentTarget.value);
            e.currentTarget.value = "";
          }
        }}
      />
      <button
        disabled={inProgress}
        className={buttonStyle}
        onClick={(e) => {
          const input = e.currentTarget
            .previousElementSibling as HTMLInputElement;
          handleSubmit(input.value);
          input.value = "";
        }}
      >
        Ask
      </button>
    </div>
  );
}

<CopilotKit>
  <CopilotSidebar Input={CustomInput} />
</CopilotKit>;
</ImageAndCode>

Actions

Actions allow the LLM to interact with your application's functionality. When an action is called by the LLM, you can provide custom components to visualize its execution and results. This example demonstrates a calendar meeting card implementation.

<ImageAndCode preview="https://cdn.copilotkit.ai/docs/copilotkit/images/render-only-example.png">
tsx
"use client"; // only necessary if you are using Next.js with the App Router.
import { useFrontendTool } from "@copilotkit/react-core/v2";

// Your custom components (examples - implement these in your app)
import { LoadingView } from "./loading-view"; // Your loading component
import {
  CalendarMeetingCardComponent,
  type CalendarMeetingCardProps,
} from "./calendar-meeting-card"; // Your meeting card component

export function YourComponent() {
  useFrontendTool({
    name: "showCalendarMeeting",
    description: "Displays calendar meeting information",
    parameters: [
      {
        name: "date",
        type: "string",
        description: "Meeting date (YYYY-MM-DD)",
        required: true,
      },
      {
        name: "time",
        type: "string",
        description: "Meeting time (HH:mm)",
        required: true,
      },
      {
        name: "meetingName",
        type: "string",
        description: "Name of the meeting",
        required: false,
      },
    ],
    render: ({ status, args }) => {
      const { date, time, meetingName } = args;

      if (status === "inProgress") {
        return <LoadingView />; // Your own component for loading state
      } else {
        const meetingProps: CalendarMeetingCardProps = {
          date: date,
          time,
          meetingName,
        };
        return <CalendarMeetingCardComponent {...meetingProps} />;
      }
    },
  });

  return <>...</>;
}
</ImageAndCode>

Agent State

The Agent State component allows you to visualize the internal state and progress of your CoAgents. When working with CoAgents, you can provide a custom component to render the agent's state. This example demonstrates a progress bar that updates as the agent runs.

<Callout title="Not started with CoAgents yet?"> If you haven't gotten started with CoAgents yet, you can get started in 10 minutes with the [quickstart guide](/coagents/quickstart/langgraph). </Callout> <ImageAndCode preview="https://cdn.copilotkit.ai/docs/copilotkit/images/coagents/AgenticGenerativeUI.gif">
tsx
"use client"; // only necessary if you are using Next.js with the App Router.

import { useCoAgentStateRender } from "@copilotkit/react-core/v2";
import { Progress } from "./progress";

type AgentState = {
  logs: string[];
};

useCoAgentStateRender<AgentState>({
  name: "basic_agent",
  render: ({ state, nodeName, status }) => {
    if (!state.logs || state.logs.length === 0) {
      return null;
    }

    // Progress is a component we are omitting from this example for brevity.
    return <Progress logs={state.logs} />;
  },
});
</ImageAndCode>

Reasoning Message

Models like OpenAI o1, o3, and o4-mini emit reasoning tokens (chain-of-thought traces). CopilotKit renders them automatically in a collapsible card. You can fully replace it or swap individual sub-components (header, content, toggle) via slot props.

For a complete guide, see Reasoning Messages.

<ImageAndCode>
tsx
import { type CopilotChatReasoningMessageProps } from "@copilotkit/react-ui";
import { CopilotKit } from "@copilotkit/react-core";
import { CopilotSidebar } from "@copilotkit/react-ui";
import "@copilotkit/react-ui/v2/styles.css";

function CustomReasoningMessage({
  message,
  messages,
  isRunning,
}: CopilotChatReasoningMessageProps) {
  const isLatest = messages?.[messages.length - 1]?.id === message.id;
  const isStreaming = !!(isRunning && isLatest);

  if (!message.content && !isStreaming) return null;

  // [!code highlight:8]
  return (
    <details open={isStreaming} className="my-2 rounded border p-3">
      <summary className="cursor-pointer font-medium text-sm">
        {isStreaming ? "🧠 Thinking…" : "💡 View reasoning"}
      </summary>
      <p className="mt-2 text-sm text-gray-600 whitespace-pre-wrap">
        {message.content}
      </p>
    </details>
  );
}

<CopilotKit>
  <CopilotSidebar
    messageView={{
      reasoningMessage: CustomReasoningMessage,
    }}
  />
</CopilotKit>;

Key concepts

  • message — The reasoning message object. message.content holds the thinking text.
  • messages — All messages in the conversation, used to determine if this is the latest message.
  • isRunning — Whether the agent is currently running.
  • Slot props — Instead of replacing the whole component, you can override just the header, contentView, or toggle sub-components. See the full guide.
</ImageAndCode>