---
title: Custom Chat Components
description: Override the composer, assistant messages, and user messages.
---

You can customize specific UI surfaces without rebuilding the full chat stack:

- `composer`
- `assistantMessage`
- `userMessage`

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

```tsx
function MyComposer({ onSend, onCancel, isRunning }) {
  // your UI
}

<Copilot apiUrl="/api/chat" composer={MyComposer} />;
```

### `ComposerProps`

```ts
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

```tsx
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

- `assistantMessage` replaces the default assistant wrapper, including the avatar/container UI.
- `userMessage` replaces the default user bubble wrapper.
- If you pass `componentLibrary` and also pass `assistantMessage`, your custom component takes priority. That means you are responsible for rendering any structured assistant content yourself.
- `composer` should handle both `isRunning` and `isLoadingMessages` so 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.content` is always a simple string.

{/* add visual: image showing the default assistant bubble beside a custom assistant bubble implementation */}

## Related guides

- [Headless Intro](/docs/chat/headless-intro)
- [Custom UI Guide](/docs/chat/custom-ui-guide)
- [GenUI](/docs/chat/genui)
