Custom Chat Components
Override the composer, assistant messages, and user messages.
You can customize specific UI surfaces without rebuilding the full chat stack:
composerassistantMessageuserMessage
These props replace the built-in UI entirely for that surface. If you override them, your component becomes responsible for rendering the message or composer state correctly.
Use these props when you want to swap a specific surface while keeping the built-in layout and state model. If you need to redesign the whole chat shell, use the headless APIs instead.
Custom composer
function MyComposer({ onSend, onCancel, isRunning }) {
// your UI
}
<Copilot apiUrl="/api/chat" composer={MyComposer} />;ComposerProps
type ComposerProps = {
onSend: (message: string) => void;
onCancel: () => void;
isRunning: boolean;
isLoadingMessages: boolean;
};Call onSend(text) when the user submits. Use onCancel() to stop a running response.
Even a simple custom composer should still account for both isRunning and isLoadingMessages, because the composer may need to disable input while streaming or while history is still loading.
Custom assistant and user messages
function AssistantBubble({ message }) {
return <div>{message.content}</div>;
}
function UserBubble({ message }) {
return <div>{String(message.content)}</div>;
}
<Copilot apiUrl="/api/chat" assistantMessage={AssistantBubble} userMessage={UserBubble} />;The message prop is the full AssistantMessage or UserMessage object from @openuidev/react-headless.
Important behavior notes
assistantMessagereplaces the default assistant wrapper, including the avatar/container UI.userMessagereplaces the default user bubble wrapper.- If you pass
componentLibraryand also passassistantMessage, your custom component takes priority. That means you are responsible for rendering any structured assistant content yourself. composershould handle bothisRunningandisLoadingMessagesso the input behaves correctly while streaming or loading history.- If your custom assistant renderer only handles plain text, document that constraint in your app and avoid assuming
message.contentis always a simple string.