# Chat Message

> A React AI chat message: the question in a bubble, the answer with its thinking, tool calls, markdown, sources and actions.

- Type: React component (AI Kit)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add chat-message`
- Docs: https://wingo-ui.com/components/chat-message

## Interaction

Your question sits in a bubble on the right, the answer reads on the left with a small mark. While it streams, three dots wait for the first word, the thinking row shimmers, tools spin inline and a caret follows the text; the actions appear when it finishes. Under the answer: Copy (a check confirms it), thumbs up or down (the pressed one fills and pops, the other steps aside and "Thanks" shows for a moment), Regenerate, the "‹ 2 / 3 ›" version switcher and the "⋯" menu. On the latest answer the actions always show; elsewhere they fade in on hover or focus, and on a phone a tap on the message shows them and its time (one message at a time) while a long press opens an action sheet with the same actions. Edit turns your bubble into a text box: Cmd or Ctrl + Enter sends, Escape cancels and puts focus back on Edit. Images in attachments open a full-screen viewer with swipe, files and sources open in a new tab, long questions fold behind "Show more", and a failed answer shows its error with Retry. Every action is a button in the Tab order with its own name; thumbs are toggles.

## Installation

```bash
npx wingo-ui@latest add chat-message
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/chat-message` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/chat-message.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Markdown](https://wingo-ui.com/components/markdown) ([Markdown](https://wingo-ui.com/components/markdown.md))
- [Reasoning Indicator](https://wingo-ui.com/components/reasoning-indicator) ([Markdown](https://wingo-ui.com/components/reasoning-indicator.md))
- [Tool Call Timeline](https://wingo-ui.com/components/tool-call-timeline) ([Markdown](https://wingo-ui.com/components/tool-call-timeline.md))
- [Action Sheet](https://wingo-ui.com/components/action-sheet) ([Markdown](https://wingo-ui.com/components/action-sheet.md))
- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Collapsible](https://wingo-ui.com/components/collapsible) ([Markdown](https://wingo-ui.com/components/collapsible.md))
- [Copy Button](https://wingo-ui.com/components/copy-button) ([Markdown](https://wingo-ui.com/components/copy-button.md))
- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))
- [Image Lightbox](https://wingo-ui.com/components/image-lightbox) ([Markdown](https://wingo-ui.com/components/image-lightbox.md))
- [Textarea](https://wingo-ui.com/components/textarea) ([Markdown](https://wingo-ui.com/components/textarea.md))
- [Tooltip](https://wingo-ui.com/components/tooltip) ([Markdown](https://wingo-ui.com/components/tooltip.md))
- [AI Chat](https://wingo-ui.com/components/ai-chat) ([Markdown](https://wingo-ui.com/components/ai-chat.md))
- [File types](https://wingo-ui.com/components/file-types) ([Markdown](https://wingo-ui.com/components/file-types.md))
- [Relative time](https://wingo-ui.com/components/relative-time) ([Markdown](https://wingo-ui.com/components/relative-time.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useLongPress](https://wingo-ui.com/components/use-long-press) ([Markdown](https://wingo-ui.com/components/use-long-press.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { ChatMessage, type ChatMessageData, type ChatMessageLabels } from "@/components/ui/chat-message"

const labels: Partial<ChatMessageLabels> = {
  assistant: "Summit Assistant",
  assistantWrote: (name) => `${name} replied`,
  regenerate: "Try again",
  good: "Helpful",
  bad: "Not helpful",
  feedbackThanks: "Thanks for the feedback",
}

export function Answer({ message, onRegenerate }: { message: ChatMessageData; onRegenerate: () => Promise<void> }) {
  return (
    <ChatMessage
      {...message}
      isLatest
      labels={labels}
      onRegenerate={onRegenerate}
      onFeedbackChange={(feedback) => console.log(message.id, feedback)}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `role?` | `"user" \| "assistant" \| "system"` | `"assistant"` | Who wrote it. user: the person's bubble with Copy and Edit. assistant: the answer with its thinking, tools, sources and response actions. system: a quiet centered line between hairlines, without avatar or actions. |
| `content?` | `string` | `""` | The message as markdown (tables, lists, code). Empty on a finished assistant message renders only the footer; empty while pending or streaming shows the typing dots. The playground falls back to the canonical invoice answer. |
| `status?` | `"pending" \| "streaming" \| "complete" \| "stopped" \| "error"` | `"complete"` | pending: sent, nothing back yet (typing dots). streaming: text arriving (caret, aria-busy, actions hidden). stopped: a Stopped note in the meta line, actions still work. error: the error row with Retry. |
| `model?` | `string` | `""` | The model that wrote the answer, shown in the meta line (a mono chip in the flat header). |
| `error?` | `ReactNode` | `""` | The message of the error row when status is "error"; empty shows labels.error. |
| `edited?` | `boolean` | `false` | Adds "edited" to the meta line. |
| `variant?` | `"default" \| "bubbles" \| "flat"` | `"default"` | default: the person in a bubble on the right, the answer as text on the left (ChatGPT). bubbles: both sides in bubbles (a messenger, a support widget). flat: both on the left with avatars and name headers, no bubbles (team threads, docs). |
| `size?` | `"sm" \| "md"` | `"md"` | Text 14 / 15px, bubble padding and avatars 24 / 28px. sm fits copilot side panels. |
| `bubble?` | `"soft" \| "solid"` | `"soft"` | The person's bubble: a light tint of the tone, or a solid fill (black / white when neutral). |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The color of the person's bubble and the edit Send button. Neutral is gray (soft) or black / white (solid). |
| `color?` | `string` | `""` | Any CSS color for the bubble; replaces the tone. Text on a solid fill turns black or white by lightness. |
| `showAvatar?` | `"assistant" \| "all" \| "none"` | `"assistant"` | Which side shows an avatar. The flat variant always shows both unless "none". |
| `assistantAvatar?` | `ReactNode` |  | Replaces the assistant's round Sparkles mark: a MatrixOrb, a logo, an image. |
| `assistantName?` | `string` | `""` | The assistant's name in the flat header and the screen reader heading; falls back to author.name, then labels.assistant. |
| `timestamp?` | `"hover" \| "always" \| "never"` | `"hover"` | The time (and model) in the meta line: shown on hover, focus or a tap, always, or never. The flat header always shows the time. |
| `timeFormat?` | `"relative" \| "time" \| "date" \| "datetime"` | `"time"` | "16:35", or relative ("5 min. ago", ticking once a minute), a date, or both. |
| `actions?` | `"auto" \| "always" \| "hover" \| "never"` | `"auto"` | auto: always visible on the latest assistant message, on hover, focus or a tap elsewhere. always / hover force one rule; never hides the action bar (the long-press sheet still works). |
| `isLatest?` | `boolean` | `false` | The last assistant message of a thread (chat-thread sets it). Drives actions="auto". |
| `grouped?` | `boolean` | `false` | A follow-up from the same sender: the avatar and header hide (the text stays aligned) and the bubble tightens its corner on the sender's side. |
| `copyFormat?` | `"markdown" \| "text"` | `"markdown"` | What Copy puts on the clipboard: the markdown source, or plain text through markdownToText. |
| `caret?` | `"dot" \| "bar" \| "block" \| "none"` | `"dot"` | The caret at the end of streaming text. |
| `codeAccent?` | `string` | `""` | The accent of code blocks in the answer and the tool inputs and outputs; empty keeps the code block's own. |
| `maxUserLines?` | `number` | `8` | Long questions fold past this many lines behind "Show more"; 0 never folds. |
| `longPress?` | `boolean` | `true` | On touch screens a long press on the message opens an action sheet with every action (and a haptic tick). Links, code and the buttons keep their own touch behavior. |
| `locale?` | `string` | `""` | Times, durations and file sizes. Empty uses UIProvider's locale, then the browser's. |
| `createdAt?` | `Date \| string \| number` |  | When it was sent; the meta line, the flat header and the screen reader heading show it. |
| `author?` | `ChatPerson` |  | { id, name, avatar? }: the person (their avatar and name) or the assistant (its name and picture). |
| `attachments?` | `ChatAttachment[]` |  | Images in a grid that opens the full-screen viewer (one large, two to four as tiles, more as +N), files as tiles with their type color and size. |
| `reasoning?` | `ChatReasoning` |  | { content, status?, startedAt?, endedAt?, durationMs? }: the thinking row above the answer (ReasoningIndicator). |
| `toolCalls?` | `ToolCall[]` |  | The tools it used, as an inline summary that expands into the timeline (ToolCallTimeline variant inline). |
| `sources?` | `ChatSource[]` |  | Chips under the answer with favicon, domain and number; they open their url in a new tab. |
| `id?` | `string` |  | The message id, set on the root; keeps the tap-to-show state per message. |
| `branch?` | `{ index: number; count: number }` |  | Which version this is (1-based) of how many: shows the "‹ 2 / 3 ›" switcher when count is above 1, and the text cross-fades on every switch. |
| `onBranchChange?` | `(index: number) => void` |  | The switcher's previous / next, with the new 1-based index. Without it the arrows are disabled. |
| `onCopy?` | `(text: string) => void` |  | After copying (Copy is built in). |
| `onRegenerate?` | `() => (void \| Promise<unknown>)` |  | Shows Regenerate on assistant messages; a returned promise keeps it spinning. |
| `onEdit?` | `(content: string) => (void \| Promise<unknown>)` |  | Shows Edit on user messages; Send calls it with the new text (unchanged text just closes the editor). A promise keeps Send loading and the editor open until it resolves. |
| `feedback?` | `"up" \| "down" \| null` |  | Controlled thumbs; without onFeedbackChange they are read-only (in the bar and the long-press sheet). The thumbs show when any of feedback, defaultFeedback or onFeedbackChange is set. |
| `defaultFeedback?` | `"up" \| "down" \| null` | `""` | The starting thumbs while uncontrolled; unset (null) shows neither pressed. |
| `onFeedbackChange?` | `(feedback: "up" \| "down" \| null) => (void \| Promise<unknown>)` |  | Pressing the pressed thumb clears it. Optimistic: a rejected promise rolls the choice back. |
| `onRetry?` | `() => (void \| Promise<unknown>)` |  | Shows Retry in the error row; a promise keeps it loading. |
| `menu?` | `MenuEntry[]` |  | The "⋯" menu (Share, Read aloud, Report, Delete): a dropdown on desktop, a bottom sheet on phones; its items also join the long-press sheet. |
| `onSourceClick?` | `(source: ChatSource) => void` |  | Replaces opening the source url. |
| `onAttachmentClick?` | `(attachment: ChatAttachment) => void` |  | Replaces the default: images open the viewer, files open their url. |
| `now?` | `Date \| number` |  | Pins the clock for relative times and live durations (screenshots, tests). Default: a shared ticker. |
| `markdownProps?` | `Partial<MarkdownProps>` |  | Passed to the Markdown of the content (labels, linkTarget, components). Keep it stable. |
| `reasoningProps?` | `Partial<ReasoningIndicatorProps>` |  | Passed to the thinking row (labels, variant, peek). Keep it stable. |
| `toolCallProps?` | `Partial<ToolCallTimelineProps>` |  | Passed to the inline tool timeline (labels, onApprove, onReject). Keep it stable. |
| `labels?` | `Partial<ChatMessageLabels>` |  | Every visible and spoken string: you, assistant, youWrote, personWrote(name), assistantWrote(name), system, copy, copied, regenerate, edit, save, cancel, good, bad, feedbackThanks, more, retry, error, stopped, edited, sources, attachments, previous, next, branch(index, count), branchLabel(index, count), showMore, showLess, thinking, attachment(name, size), openImage(name), moreImages(count). English by default (see Parts). |
| `classNames?` | `Partial<Record<"avatar" \| "header" \| "attachments" \| "bubble" \| "reasoning" \| "tools" \| "content" \| "typing" \| "error" \| "sources" \| "footer" \| "actions" \| "action" \| "meta" \| "editor", string>>` |  | Extra classes for the inner parts. |

## Parts

### ChatMessageActions (component)

The action bar alone (copy, edit, thumbs, regenerate, versions, menu), for custom message layouts. Props: ChatMessageActionsProps.

### ChatMessageAttachments (component)

The image grid with its viewer and the file tiles. Props: ChatMessageAttachmentsProps.

### ChatMessageSources (component)

The rail of source chips. Props: ChatMessageSourcesProps.

### DEFAULT_CHAT_MESSAGE_LABELS (constant)

Every English string of the message, its action bar, sources, attachments and versions, to spread under a translation.

### ChatMessageTyping (component)

The three typing dots of a pending reply, with sr-only text (chat-thread's thinking row uses it). Props: ChatMessageTypingProps.

## Examples

- **Streaming**: A reply from the first token: typing dots, the thinking row, two tools, then text with a caret.
- **Bubbles**: A support widget: variant="bubbles", size sm and a solid brand bubble for the customer.
- **Flat thread**: Team thread with avatars, name headers, a system line and an orb as the assistant's avatar.
- **Error and stopped**: An inline error with Retry that succeeds on the second try, and an answer stopped halfway.
- **Attachments and sources**: Four photos in a grid that opens the viewer, a PDF tile, and three source chips.
- **Editing and versions**: Edit a question in place; each version keeps its own answer behind the ‹ › switcher.
- **Phone**: At 390px: tap to show actions, long press for the action sheet, the menu as a bottom sheet.

## See also

- [Chat Thread](https://wingo-ui.com/components/chat-thread) ([Markdown](https://wingo-ui.com/components/chat-thread.md))
- [Markdown](https://wingo-ui.com/components/markdown) ([Markdown](https://wingo-ui.com/components/markdown.md))
- [Reasoning Indicator](https://wingo-ui.com/components/reasoning-indicator) ([Markdown](https://wingo-ui.com/components/reasoning-indicator.md))
- [Tool Call Timeline](https://wingo-ui.com/components/tool-call-timeline) ([Markdown](https://wingo-ui.com/components/tool-call-timeline.md))
- [Prompt Input](https://wingo-ui.com/components/prompt-input) ([Markdown](https://wingo-ui.com/components/prompt-input.md))
- [AI Chat](https://wingo-ui.com/components/ai-chat) ([Markdown](https://wingo-ui.com/components/ai-chat.md))

---

Canonical: https://wingo-ui.com/components/chat-message
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
