docs/snippets/shared/guides/custom-look-and-feel/bring-your-own-components.mdx
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.
| Component | Description |
|---|---|
| UserMessage | Message component for user messages |
| AssistantMessage | Message component for assistant messages |
| Window | Contains the chat |
| Button | Button that opens/closes the chat |
| Header | The header of the chat |
| Messages | The chat messages area |
| Suggestions | Customize how suggestions are displayed |
| Input | The chat input |
| Actions | Customize how actions (tools) are displayed |
| Agent State | Customize how agent state messages are displayed |
| Reasoning Message | Customize how model reasoning/thinking is displayed |
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.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>
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>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">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>
The CopilotSidebar and CopilotPopup components allow you to customize their trigger button by passing in a custom Button component.
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>
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">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>
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">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>
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>
```
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">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>
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">"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 (
<>...</>
);
}
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">"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} />;
},
});
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>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.header, contentView, or toggle sub-components. See the full guide.