# MessageBubble

One message in a conversation: the bubble with its text, an optional avatar and author, the quote of the message it answers, reactions, a footer with the time, edited, pinned and the delivery status, and a toolbar of actions on hover. Put messages in a MessageList; use plain for long replies from an assistant.

**Dependencies:** npm: 2 direct, 5 total; registry: 3 direct, 5 total.

## Usage

### When to use

- Show one message in a chat between people, or with an assistant, with its sender, time and status.
- Group messages that follow one from the same sender.

### When not to use

- A comment on a page or a review. Use Card or plain text.
- A short status line in the interface. Use Alert or Toast.
- A tip on a control. Use Tooltip.

## Variants

### side

start for messages from others, end for the reader's own.

Values: `start`, `end`. Default: `start`.

### variant

default: a neutral bubble with an edge. accent: the reader's own messages in a chat between people. plain: no fill, for a long reply from an assistant.

Values: `default`, `accent`, `plain`. Default: `default`.

### status

The delivery status of the reader's own message: sending, sent (the server has it), delivered (it reached the other device), read (seen) or failed.

Values: `sending`, `sent`, `delivered`, `read`, `failed`. Default: `undefined`.

## Examples

### Two people

```tsx
<div className="flex w-full max-w-md flex-col gap-md">
  <MessageBubble author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:41">Can you send the brief before the call?</MessageBubble>
  <MessageBubble side="end" variant="accent" time="10:42" status="read">Sure, sending it now.</MessageBubble>
</div>
```

### Grouped messages

Messages that follow one from the same author drop the name and avatar and sit closer.

```tsx
<div className="flex w-full max-w-md flex-col gap-md">
  <MessageBubble author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>}>The brief looks good.</MessageBubble>
  <MessageBubble grouped author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:45">One question about the timeline, though.</MessageBubble>
</div>
```

### Delivery states

The reader's own messages show where they are: sending, sent, delivered, seen, or not sent with a way to retry.

```tsx
<div className="flex w-full max-w-md flex-col gap-md">
  <MessageBubble side="end" variant="accent" time="10:46" status="sending">On my way.</MessageBubble>
  <MessageBubble side="end" variant="accent" time="10:46" status="sent">Running five minutes late.</MessageBubble>
  <MessageBubble side="end" variant="accent" time="10:47" status="delivered">Order me a coffee?</MessageBubble>
  <MessageBubble side="end" variant="accent" time="10:47" status="read">Thanks!</MessageBubble>
  <MessageBubble side="end" variant="accent" time="10:48" status="failed" onRetry={retrySend}>See you at 3.</MessageBubble>
</div>
```

### Actions on hover

Hover or focus the message to show its toolbar: react, reply, forward, pin and select.

```tsx
<MessageBubble value="m1" author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:41" toolbar={<MessageActions onReact={addReaction} onReply={replyToMessage} onForward={forwardMessage} onPinnedChange={setPinned} onSelect={startSelection} />} className="max-w-md pt-xl">Can you send the brief before the call?</MessageBubble>
```

### Reactions, reply and more

A reply quotes the message it answers; reactions toggle; edited, pinned and forwarded show as notes.

```tsx
<div className="flex w-full max-w-md flex-col gap-md">
  <MessageBubble forwarded author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:41" reactions={sampleReactions} onReactionToggle={toggleReaction}>The launch moved to Friday.</MessageBubble>
  <MessageBubble side="end" variant="accent" edited pinned time="10:43" status="read" replyTo={sampleReply}>Then we ship the brief on Thursday.</MessageBubble>
</div>
```

### Assistant reply

Plain has no fill, so a long reply reads as page text, with actions under it.

```tsx
<MessageBubble variant="plain" author="Assistant" avatar={<Avatar alt="">
  <IconSparkles size={16} />
</Avatar>} actions={<Button size="sm" variant="ghost" iconOnly aria-label="Copy">
  <IconCopy size={16} />
</Button>} className="max-w-md">The report covers three regions. Sales grew in two of them, and the third held steady.</MessageBubble>
```

## Do and don't

### Do

- Use side end with variant accent for the reader's own messages in a chat between people, and side start with default for everyone else.
- Use variant plain for an assistant's replies, which are often long and hold lists or code.
- Set grouped on messages that follow one from the same author within a few minutes.
- Pass a MessageActions as toolbar with only the actions the conversation supports, and show reactions under the bubble.
- Keep a failed message in place with status failed and onRetry, instead of removing it.

### Don't

- Do not put a Card or another bubble inside a bubble.
- Do not show the time on every grouped message; show it on the last one of the group.
- Do not tell the sides apart by color only; keep start and end.
- Do not hide an action behind hover alone: MessageActions also shows on focus and on a tap.

## Props

| Name | Type | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `value` | `string` |  |  | Identifies the message for selection in a MessageList. |
| `side` | `enum` | `start`, `end` | `start` | start for messages from others, end for the reader's own. |
| `variant` | `enum` | `default`, `accent`, `plain` | `default` | default: a neutral bubble with an edge. accent: the reader's own messages in a chat between people. plain: no fill, for a long reply from an assistant. |
| `author` | `node` |  |  | The sender's name above the bubble. Hidden on a grouped message, but still read. |
| `avatar` | `node` |  |  | An Avatar beside the bubble. Pass alt="" when author names the sender. |
| `time` | `node` |  |  | The time as shown, such as 10:42. |
| `dateTime` | `string` |  |  | The machine-readable time for the time element, such as 2026-10-23T10:42. |
| `status` | `enum` | `sending`, `sent`, `delivered`, `read`, `failed` |  | The delivery status of the reader's own message: sending, sent (the server has it), delivered (it reached the other device), read (seen) or failed. |
| `onRetry` | `() => void` |  |  | With status failed: shows a Retry button that calls it. |
| `edited` | `boolean` |  | `false` | The message was changed after it was sent: Edited shows in the footer. |
| `pinned` | `boolean` |  | `false` | The message is pinned in the conversation: a pin shows in the footer. |
| `forwarded` | `boolean` |  | `false` | The message was forwarded from another conversation: Forwarded shows above it. |
| `replyTo` | `{ author: React.ReactNode; text: React.ReactNode; onClick?: () => void }` |  |  | The message this one answers, quoted above the bubble. onClick makes the quote a button that scrolls to it. |
| `reactions` | `{ emoji: string; count: number; reacted?: boolean }[]` |  |  | Reactions under the bubble, as chips with counts. reacted marks the reader's own. |
| `onReactionToggle` | `(emoji: string) => void` |  |  | Makes each reaction chip a toggle button that adds or removes the reader's reaction. |
| `toolbar` | `node` |  |  | A MessageActions shown over the bubble on hover, focus or a tap. Hidden in selection mode. |
| `actions` | `node` |  |  | Controls in the footer, such as a Copy button. Use ghost Buttons in size sm. For actions on hover, use toolbar. |
| `grouped` | `boolean` |  | `false` | Continues the message above from the same author: no name or avatar, and a smaller gap. |
| `labels` | `{ sending?: string; sent?: string; delivered?: string; read?: string; failed?: string; retry?: string; edited?: string; pinned?: string; forwarded?: string; replyTo?: string; select?: string; reaction?: (emoji: string, count: number) => string }` |  |  | Every word the message shows or reads out. Defaults: Sending, Sent, Delivered, Seen, Not sent, Retry, Edited, Pinned, Forwarded, Reply to, Select message. Vietnamese: Đang gửi, Đã gửi, Đã nhận, Đã xem, Chưa gửi được, Thử lại, Đã sửa, Đã ghim, Đã chuyển tiếp, Trả lời, Chọn tin nhắn. |
| `children` (required) | `node` |  |  | The message: text, or text with links and AttachmentPreview tiles. |

## Anatomy

| Part | Description |
| --- | --- |
| `root` | One row: the selection checkbox, the avatar column, then the body. Reversed for side end. In selection mode it is padded and rounded, and takes the muted accent fill when picked. |
| `select` | The Checkbox of a message in selection mode, in a box one line tall. |
| `avatar` | Optional column for an Avatar at the outer edge. Keeps its width on a grouped message, so the bubbles line up. |
| `body` | The author, the notes, the bubble, the reactions and the footer, stacked. At most 80% of the row, except plain. |
| `author` | The sender's name above the bubble. |
| `forwarded` | A note above a forwarded message: a 12px forward icon and the word Forwarded. |
| `reply` | The quote of the message answered: its author and one line of it, behind a strong start edge. A button with onClick. |
| `bubble` | The message text. Keeps the line breaks the sender typed and wraps long words. |
| `toolbar` | The MessageActions passed in toolbar, floating over the bubble's outer top corner. |
| `reactions` | The row of reaction chips under the bubble. It wraps. |
| `reaction` | One chip: the emoji and its count, 24px tall with a 44px touch target. Pressed, in the muted accent fill, when the reader reacted. |
| `footer` | The time, edited, the pin, the status, Retry and the actions under the bubble. |
| `status` | The delivery status: a 12px icon with its words for screen readers, or the visible words for a failed message. |
| `icon` | The status icon: clock while sending, one check when sent, two checks when delivered, two checks in the accent icon color when seen, alert circle when it failed. |
| `retry` | The underlined text button that sends a failed message again. |
| `actions` | Optional controls in the footer, such as a Copy button. |

## Accessibility

Role: `generic`

| Keys | Action |
| --- | --- |
| `Tab` | Moves to the quote, the toolbar, the reactions, Retry and the footer actions, in that order. The toolbar is one tab stop. |
| `Arrow Left / Arrow Right` | In the toolbar, moves between its buttons (MessageActions). |
| `Space` | In selection mode, on the checkbox, picks the message or puts it back. Shift with a click picks a range. |

- The author name comes before the text, so a screen reader hears who wrote it first. On a grouped message the name is hidden but still read.
- Pass alt="" to the Avatar when author names the same person, so the name is not read twice.
- The time is a time element; pass dateTime with the machine-readable value.
- The status icon is hidden from screen readers; the status is read in words from labels (EN `Sent`, `Delivered`, `Seen`, VI `Đã gửi`, `Đã nhận`, `Đã xem`). A failed message shows its words, EN `Not sent`, in the danger color, with a Retry button when onRetry is set.
- Seen and delivered share the two-check icon; seen takes the accent icon color, and the words tell them apart for screen readers.
- The quote of the message answered is read with EN `Reply to` before its author. With onClick it is a button that scrolls to that message.
- Each reaction chip is a toggle button with aria-pressed, named EN `👍 2`; without onReactionToggle the chips are text.
- The toolbar shows on hover, while anything in the message has focus, while its emoji popover is open, and after a tap on a touch screen. It is never the only way to an action: keyboard focus reveals it.
- In selection mode each message gets a checkbox named EN `Select message` and described by the message text; a press anywhere on the row toggles it, and the picked row takes the muted accent fill.
- Color never carries the side on its own: the bubble also sits at the start or the end of the row.

## Install

```bash
npx werewolf-ui add message-bubble
```

Run `npx werewolf-ui init` once in your app before adding components. Init sets up tokens, their CSS import and agent rules. Add installs this component and its dependencies while preserving your theme. See [Installation](https://www.werewolf-ui.dev/docs/installation.md) for requirements and the alternative registry setup.
