# MessageList

The scrolling log of a conversation: MessageBubbles in order, oldest first, with a typing indicator after the last one. It opens at the newest message and follows new ones while the reader is at the end; once they scroll up it stays put and offers a button back down.

**Dependencies:** npm: 1 direct, 7 total; registry: 5 direct, 9 total.

## Usage

### When to use

- Show the messages of a conversation in order, with new ones followed while the reader is at the end.
- Show that the other person or an assistant is writing a reply.

### When not to use

- A feed of posts or events that is not a conversation. Use a list of Cards.
- Notifications that come and go. Use Toast.
- The steps an agent took. Use a list, or Stepper for a flow in order.

## Variants

### typingVariant

default draws the dots in a neutral bubble, like a message from a person; plain draws them without one, for an assistant that writes plain replies.

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

## Examples

### Conversation

```tsx
<MessageList label="Messages with Linh" className="h-80 w-full max-w-md rounded-container border border-border">
  <Separator label="Today" />
  <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>
  <MessageBubble author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:44">Got it, thanks!</MessageBubble>
</MessageList>
```

### Typing

The indicator sits after the last message; its words go to a status region.

```tsx
<MessageList typing typingLabel="Linh is typing" typingAvatar={<Avatar alt="" color="blue">LT</Avatar>} label="Messages with Linh" className="h-64 w-full max-w-md rounded-container border border-border">
  <MessageBubble side="end" variant="accent" time="10:42" status="read">Are we still on for 3?</MessageBubble>
</MessageList>
```

### Several people typing

typingUsers names everyone writing; the words say who, and the first avatar lines up with their messages.

```tsx
<MessageList typingUsers={typingPeople} label="Design team" className="h-64 w-full max-w-md rounded-container border border-border">
  <MessageBubble author="Minh" avatar={<Avatar alt="" color="purple">M</Avatar>} time="09:12">The new icons are in.</MessageBubble>
</MessageList>
```

### Assistant thinking

typingVariant plain draws the dots without a bubble, where the plain reply will appear.

```tsx
<MessageList typing typingVariant="plain" typingLabel="Assistant is writing" typingAvatar={<Avatar alt="">
  <IconSparkles size={16} />
</Avatar>} label="Chat with the assistant" className="h-64 w-full max-w-lg rounded-container border border-border">
  <MessageBubble side="end">Summarize the October report.</MessageBubble>
</MessageList>
```

### Assistant

Replies from an assistant use plain bubbles; the reader's own messages stay in neutral bubbles at the end.

```tsx
<MessageList label="Chat with the assistant" className="h-80 w-full max-w-lg rounded-container border border-border">
  <MessageBubble side="end">Summarize the October report.</MessageBubble>
  <MessageBubble variant="plain" author="Assistant" avatar={<Avatar alt="">
  <IconSparkles size={16} />
</Avatar>}>Sales grew in two of three regions. The third held steady, and returns fell for the second month in a row.</MessageBubble>
</MessageList>
```

### Pick messages

In selection mode every message has a checkbox; a press on a row picks it, and Shift picks a range.

```tsx
<MessageList selectionMode defaultSelected={pickedMessages} onSelectionModeChange={setSelectionMode} label="Messages with Linh" className="h-80 w-full max-w-md rounded-container border border-border">
  <MessageBubble value="m1" author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:41">Can you send the brief before the call?</MessageBubble>
  <MessageBubble value="m2" side="end" variant="accent" time="10:42" status="read">Sure, sending it now.</MessageBubble>
  <MessageBubble value="m3" author="Linh Tran" avatar={<Avatar alt="" color="blue">LT</Avatar>} time="10:44">Got it, thanks!</MessageBubble>
</MessageList>
```

## Do and don't

### Do

- Give the list a fixed or flexible height, such as h-96 or flex-1 in a column, so it scrolls instead of growing the page.
- Turn typing on only while someone is writing: pass typingUsers from your realtime events (the other side's Composer onTypingChange), and clear it when their message arrives.
- Split days with a Separator label, such as Today.
- For selection, give every MessageBubble a value, and show the actions for the picked messages (Forward, Delete) in a bar beside the list.

### Don't

- Do not scroll the reader down while they read older messages; leave stickToBottom on and let the jump button bring them back.
- Do not use a Spinner for typing; the typing indicator is part of the list.
- Do not put the Composer inside the list; place it below.

## Props

| Name | Type | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `label` (required) | `string` |  |  | Names the log for screen readers, such as Messages with Linh. |
| `typing` | `boolean` |  | `false` | Shows the typing indicator after the last message and says who is typing once. typingUsers turns it on too. |
| `typingUsers` | `{ name: string; avatar?: React.ReactNode }[]` |  |  | The people typing right now. Any entry turns typing on and names them beside the dots (EN `Linh and An are typing`). One person's avatar lines up with their messages; two or more overlap in an AvatarStack, three at most and +N. |
| `typingLabel` | `string` |  |  | The words for who is typing, instead of the ones built from typingUsers, such as Linh is typing. |
| `typingAvatar` | `node` |  |  | An Avatar beside the typing indicator, so it lines up with that person's messages. Pass alt="". |
| `typingVariant` | `enum` | `default`, `plain` | `default` | default draws the dots in a neutral bubble, like a message from a person; plain draws them without one, for an assistant that writes plain replies. |
| `stickToBottom` | `boolean` |  | `true` | Follows new messages, and replies that grow as they stream in, while the reader is at the end. |
| `selectionMode` | `boolean` |  | `false` | Shows a checkbox on every MessageBubble with a value. A press on a message picks it; Shift with a click picks every message between it and the last one picked. |
| `onSelectionModeChange` | `(selectionMode: boolean) => void` |  |  | Called with false when Escape is pressed in selection mode, so you can leave it. |
| `selected` | `string[]` |  |  | Values of the picked messages, for controlled selection. |
| `defaultSelected` | `string[]` |  |  | Values picked at first render, for uncontrolled selection. |
| `onSelectedChange` | `(selected: string[]) => void` |  |  | Called with the picked values, in the order of the messages. |
| `labels` | `{ jumpToLatest?: string; selected?: (count: number) => string; typing?: (names: string[]) => string }` |  |  | The name of the jump button and the words announced for the selection and for who is typing. Defaults: Jump to latest, 2 selected, Typing, Linh is typing, Linh and An are typing, Linh, An and 2 others are typing. Vietnamese: Tới tin mới nhất, Đã chọn 2, Đang soạn tin, Linh đang soạn tin, Linh và An đang soạn tin, Linh, An và 2 người khác đang soạn tin. |
| `children` (required) | `node` |  |  | MessageBubbles, oldest first, with a Separator label between days. |

## Anatomy

| Part | Description |
| --- | --- |
| `root` | The frame. Give it a height (h-96, flex-1); the viewport fills it. |
| `viewport` | The scrolling log. |
| `content` | The column of messages, with padding so a focus ring never touches the edge. |
| `typing` | The typing row after the last message: the typing person's avatar, or an AvatarStack of two or more, the dots and who is typing. |
| `typingBubble` | A neutral bubble one line tall with three dots; with typingVariant plain, the dots alone. |
| `typingText` | Who is typing, in words, beside the dots, in secondary text, when typingUsers names them. Drawn only: the status region reads the same words. |
| `dot` | One 6px dot. The three pulse one after the other on the duration-loop cycle and hold still under reduced motion. |
| `jump` | The secondary icon Button at the bottom that scrolls back to the newest message, shown while the reader is away from the end. |

## Accessibility

Role: `log`

| Keys | Action |
| --- | --- |
| `Tab` | Moves focus to the log, then to the links and actions in the messages. |
| `Arrow Up / Arrow Down` | With the log focused, scrolls through the messages. |
| `Page Up / Page Down, Home / End` | With the log focused, scrolls a page at a time, or to the first or the newest message. |
| `Escape` | In selection mode, calls onSelectionModeChange(false). |

- The scrolling region is a log named by label (EN `Messages with Linh`), so a screen reader reads new messages as they arrive, politely.
- The log takes focus, so the keyboard can scroll it, and shows the focus ring.
- The typing indicator is drawn only. Its words (EN `Linh is typing`, `Linh and An are typing`) go to a separate status region outside the log, so they are read once when typing starts or the people change, and never as a message.
- The jump button is named by labels.jumpToLatest (EN `Jump to latest`, VI `Tới tin mới nhất`). While the reader is at the end it is hidden and out of the tab order.
- Following new messages never moves focus. Jumping moves focus back to the log, since the button hides.
- In selection mode a second status region announces the count, EN `2 selected`, VI `Đã chọn 2`, as it changes.

## Install

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

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.
