> For AI agents: the complete documentation index is available at https://wallarm.github.io/design-system/llms.txt, the full documentation bundle is available at https://wallarm.github.io/design-system/llms-full.txt.

# Conversation

Stick-to-bottom for the Wally log: `Conversation` takes `ChatBody`’s place between `ChatHeader` and `ChatFooter`, and `ConversationContent` renders the built `ChatBody` (`role=log`) as the scroller. While an answer streams the view follows it; once the reader scrolls up it stops following and `ConversationScrollButton` appears. Scrolling back down, clicking the button or changing `autoScrollKey` (pass the last user message id) re-sticks. `ConversationEmptyState` is the greeting for an empty chat, rendered inside the scroller. The scroll button is the stock small icon-only Button, floated at the bottom centre.

[Source](https://github.com/wallarm/design-system/tree/main/packages/design-system/src/components/Conversation) · [Storybook](https://wallarm.github.io/design-system/storybook/?path=/docs/ai-conversation--overview)

```tsx
import { Fragment, useState } from 'react';
import { AssistantMessage, AssistantMessageContent } from '@wallarm-org/design-system/AssistantMessage';
import { Chat, ChatFooter, ChatHeader, ChatHeaderTitle } from '@wallarm-org/design-system/Chat';
import { ChatInput, ChatInputSubmit, ChatInputTextarea } from '@wallarm-org/design-system/ChatInput';
import { Conversation, ConversationContent, ConversationScrollButton } from '@wallarm-org/design-system/Conversation';
import { MessageActions, MessageTimestamp } from '@wallarm-org/design-system/MessageActions';
import { UserMessage, UserMessageBubble } from '@wallarm-org/design-system/UserMessage';

export const Default = () => {
  const [turns, setTurns] = useState(HISTORY);
  const [value, setValue] = useState('');
  const lastId = turns[turns.length - 1]?.id;

  const handleSubmit = (question: string) => {
    setTurns(previous => [
      ...previous,
      { id: `u${previous.length}`, question, answer: ANSWER.slice(0, 2) },
    ]);
    setValue('');
  };

  return (
    <div className='h-640 w-480'>
      <Chat data-testid='chat'>
        <ChatHeader>
          <ChatHeaderTitle />
        </ChatHeader>
        <Conversation autoScrollKey={lastId}>
          <ConversationContent>
            {turns.map((turn, index) => (
              <Fragment key={turn.id}>
                <UserMessage data-testid={`conversation--user-message-${index}`}>
                  <UserMessageBubble>{turn.question}</UserMessageBubble>
                  <MessageActions>
                    <MessageTimestamp dateTime='2026-10-21T09:41:00Z'>2m ago</MessageTimestamp>
                  </MessageActions>
                </UserMessage>
                <AssistantMessage data-testid={`conversation--assistant-message-${index}`}>
                  <AssistantMessageContent>
                    {turn.answer.map(paragraph => (
                      <p key={paragraph}>{paragraph}</p>
                    ))}
                  </AssistantMessageContent>
                  <MessageActions visible>
                    <MessageTimestamp dateTime='2026-10-21T09:42:00Z'>1m ago</MessageTimestamp>
                  </MessageActions>
                </AssistantMessage>
              </Fragment>
            ))}
          </ConversationContent>
          <ConversationScrollButton />
        </Conversation>
        <ChatFooter>
          <ChatInput value={value} onValueChange={setValue} onSubmit={handleSubmit}>
            <ChatInputTextarea />
            <ChatInputSubmit />
          </ChatInput>
        </ChatFooter>
      </Chat>
    </div>
  );
};
```

## Usage

```tsx
import { AssistantMessage, AssistantMessageContent } from '@wallarm-org/design-system/AssistantMessage';
import { Chat, ChatFooter, ChatHeader, ChatHeaderTitle } from '@wallarm-org/design-system/Chat';
import { ChatInput, ChatInputSubmit, ChatInputTextarea } from '@wallarm-org/design-system/ChatInput';
import { Conversation, ConversationContent, ConversationScrollButton } from '@wallarm-org/design-system/Conversation';
import { MessageActions, MessageTimestamp } from '@wallarm-org/design-system/MessageActions';
import { UserMessage, UserMessageBubble } from '@wallarm-org/design-system/UserMessage';
```

## Examples

### Empty

`ConversationEmptyState` with greeting pills, over the `strong` background.

```tsx
import { useState } from 'react';
import { Chat, ChatFooter, ChatHeader, ChatHeaderTitle, ChatSuggestion, ChatSuggestions } from '@wallarm-org/design-system/Chat';
import { ChatInput, ChatInputSubmit, ChatInputTextarea } from '@wallarm-org/design-system/ChatInput';
import { Conversation, ConversationContent, ConversationEmptyState, ConversationScrollButton } from '@wallarm-org/design-system/Conversation';

export const Empty = () => {
  const [value, setValue] = useState('');

  const handleSubmit = () => {
    setValue('');
  };

  return (
    <div className='h-640 w-480'>
      <Chat data-testid='chat' background='strong'>
        <ChatHeader>
          <ChatHeaderTitle />
        </ChatHeader>
        <Conversation>
          <ConversationContent>
            <ConversationEmptyState>
              <ChatSuggestions placement='greeting' label='Suggestions'>
                <ChatSuggestion data-testid='chat--suggestion-0'>
                  Why was this request blocked?
                </ChatSuggestion>
                <ChatSuggestion data-testid='chat--suggestion-1'>
                  Show top attacked endpoints
                </ChatSuggestion>
              </ChatSuggestions>
            </ConversationEmptyState>
          </ConversationContent>
          <ConversationScrollButton />
        </Conversation>
        <ChatFooter>
          <ChatInput value={value} onValueChange={setValue} onSubmit={handleSubmit}>
            <ChatInputTextarea />
            <ChatInputSubmit />
          </ChatInput>
        </ChatFooter>
      </Chat>
    </div>
  );
};
```

### Streaming

A paragraph streams in every 600ms. The view follows the growing answer; scroll up and it stops
following and the scroll button appears.

```tsx
import { Fragment, useEffect, useState } from 'react';
import { AssistantMessage, AssistantMessageContent } from '@wallarm-org/design-system/AssistantMessage';
import { Chat, ChatHeader, ChatHeaderTitle } from '@wallarm-org/design-system/Chat';
import { Conversation, ConversationContent, ConversationScrollButton } from '@wallarm-org/design-system/Conversation';
import { MessageActions, MessageTimestamp } from '@wallarm-org/design-system/MessageActions';
import { UserMessage, UserMessageBubble } from '@wallarm-org/design-system/UserMessage';

export const Streaming = () => {
  const [count, setCount] = useState(1);
  const streaming = count < ANSWER.length;

  useEffect(() => {
    if (!streaming) return;
    const timer = setInterval(() => setCount(value => value + 1), 600);
    return () => clearInterval(timer);
  }, [streaming]);

  return (
    <div className='h-640 w-480'>
      <Chat data-testid='chat'>
        <ChatHeader>
          <ChatHeaderTitle />
        </ChatHeader>
        <Conversation>
          <ConversationContent aria-busy={streaming}>
            {HISTORY.map((turn, index) => (
              <Fragment key={turn.id}>
                <UserMessage data-testid={`conversation--user-message-${index}`}>
                  <UserMessageBubble>{turn.question}</UserMessageBubble>
                  <MessageActions>
                    <MessageTimestamp dateTime='2026-10-21T09:41:00Z'>2m ago</MessageTimestamp>
                  </MessageActions>
                </UserMessage>
                <AssistantMessage data-testid={`conversation--assistant-message-${index}`}>
                  <AssistantMessageContent>
                    {turn.answer.map(paragraph => (
                      <p key={paragraph}>{paragraph}</p>
                    ))}
                  </AssistantMessageContent>
                  <MessageActions visible>
                    <MessageTimestamp dateTime='2026-10-21T09:42:00Z'>1m ago</MessageTimestamp>
                  </MessageActions>
                </AssistantMessage>
              </Fragment>
            ))}
            <AssistantMessage data-testid='conversation--streaming-message' aria-busy={streaming}>
              <AssistantMessageContent>
                {ANSWER.slice(0, count).map(paragraph => (
                  <p key={paragraph}>{paragraph}</p>
                ))}
              </AssistantMessageContent>
            </AssistantMessage>
          </ConversationContent>
          <ConversationScrollButton />
        </Conversation>
      </Chat>
    </div>
  );
};
```

### Analytics

`data-analytics-id` on `ConversationScrollButton` lands on its `<button>`. Scroll the log up to
reveal it. Keep analytics off the `Conversation` root — a click anywhere inside would resolve to it.

```tsx
import { Fragment } from 'react';
import { AssistantMessage, AssistantMessageContent } from '@wallarm-org/design-system/AssistantMessage';
import { Chat, ChatHeader, ChatHeaderTitle } from '@wallarm-org/design-system/Chat';
import { Conversation, ConversationContent, ConversationScrollButton } from '@wallarm-org/design-system/Conversation';
import { MessageActions, MessageTimestamp } from '@wallarm-org/design-system/MessageActions';
import { UserMessage, UserMessageBubble } from '@wallarm-org/design-system/UserMessage';

export const Analytics = () => (
  <div className='h-640 w-480'>
    <Chat data-testid='chat'>
      <ChatHeader>
        <ChatHeaderTitle />
      </ChatHeader>
      <Conversation>
        <ConversationContent>
          {HISTORY.map((turn, index) => (
            <Fragment key={turn.id}>
              <UserMessage data-testid={`conversation--user-message-${index}`}>
                <UserMessageBubble>{turn.question}</UserMessageBubble>
                <MessageActions>
                  <MessageTimestamp dateTime='2026-10-21T09:41:00Z'>2m ago</MessageTimestamp>
                </MessageActions>
              </UserMessage>
              <AssistantMessage data-testid={`conversation--assistant-message-${index}`}>
                <AssistantMessageContent>
                  {turn.answer.map(paragraph => (
                    <p key={paragraph}>{paragraph}</p>
                  ))}
                </AssistantMessageContent>
                <MessageActions visible>
                  <MessageTimestamp dateTime='2026-10-21T09:42:00Z'>1m ago</MessageTimestamp>
                </MessageActions>
              </AssistantMessage>
            </Fragment>
          ))}
        </ConversationContent>
        <ConversationScrollButton data-analytics-id='WALLY_SCROLL_TO_LATEST' />
      </Conversation>
    </Chat>
  </div>
);
```

## Props

| Prop           | Type                                                 | Default | Description                                                                                                                                                                                                     |
| -------------- | ---------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| threshold      | `number \| undefined`                                | `70`    | Distance in px from the bottom that still counts as "at bottom". Default 70.                                                                                                                                    |
| autoScrollKey  | `string \| number \| undefined`                      |         | When it changes, re-stick and scroll to the bottom even if the reader scrolled up. Pass the last user message id, so sending always reveals the answer. For a thread switch, remount instead: `key={threadId}`. |
| defaultChecked | `boolean \| undefined`                               |         |                                                                                                                                                                                                                 |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |         |                                                                                                                                                                                                                 |
| autoFocus      | `boolean \| undefined`                               |         |                                                                                                                                                                                                                 |
| id             | `string \| undefined`                                |         |                                                                                                                                                                                                                 |
| tabIndex       | `number \| undefined`                                |         |                                                                                                                                                                                                                 |
| title          | `string \| undefined`                                |         |                                                                                                                                                                                                                 |
| rel            | `string \| undefined`                                |         |                                                                                                                                                                                                                 |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |         |                                                                                                                                                                                                                 |
| data-testid    | `string \| undefined`                                |         |                                                                                                                                                                                                                 |

### ConversationContent

| Prop             | Type                                                 | Default | Description                                                       |
| ---------------- | ---------------------------------------------------- | ------- | ----------------------------------------------------------------- |
| contentClassName | `string \| undefined`                                |         | Classes for the inner column (`flex flex-col`, no gap — v1 §3.1). |
| defaultChecked   | `boolean \| undefined`                               |         |                                                                   |
| defaultValue     | `string \| number \| readonly string[] \| undefined` |         |                                                                   |
| autoFocus        | `boolean \| undefined`                               |         |                                                                   |
| id               | `string \| undefined`                                |         |                                                                   |
| tabIndex         | `number \| undefined`                                |         |                                                                   |
| title            | `string \| undefined`                                |         |                                                                   |
| rel              | `string \| undefined`                                |         |                                                                   |
| onChange         | `ChangeEventHandler<T, Element> \| undefined`        |         |                                                                   |
| data-testid      | `string \| undefined`                                |         |                                                                   |

### ConversationEmptyState

| Prop           | Type                                                 | Default                     | Description                                            |
| -------------- | ---------------------------------------------------- | --------------------------- | ------------------------------------------------------ |
| title          | `ReactNode`                                          | `How can I help you today?` | Greeting heading. Default 'How can I help you today?'. |
| defaultChecked | `boolean \| undefined`                               |                             |                                                        |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |                             |                                                        |
| autoFocus      | `boolean \| undefined`                               |                             |                                                        |
| id             | `string \| undefined`                                |                             |                                                        |
| tabIndex       | `number \| undefined`                                |                             |                                                        |
| rel            | `string \| undefined`                                |                             |                                                        |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |                             |                                                        |
| data-testid    | `string \| undefined`                                |                             |                                                        |

### ConversationScrollButton

| Prop           | Type                                                 | Default                    | Description                                                      |
| -------------- | ---------------------------------------------------- | -------------------------- | ---------------------------------------------------------------- |
| label          | `string \| undefined`                                | `Scroll to latest message` | Accessible name and tooltip. Default 'Scroll to latest message'. |
| title          | `string \| undefined`                                |                            |                                                                  |
| defaultChecked | `boolean \| undefined`                               |                            |                                                                  |
| defaultValue   | `string \| number \| readonly string[] \| undefined` |                            |                                                                  |
| autoFocus      | `boolean \| undefined`                               |                            |                                                                  |
| id             | `string \| undefined`                                |                            |                                                                  |
| tabIndex       | `number \| undefined`                                |                            |                                                                  |
| rel            | `string \| undefined`                                |                            |                                                                  |
| onChange       | `ChangeEventHandler<T, Element> \| undefined`        |                            |                                                                  |
| type           | `"button" \| "submit" \| "reset" \| undefined`       |                            |                                                                  |
| disabled       | `boolean \| undefined`                               |                            |                                                                  |
| name           | `string \| undefined`                                |                            |                                                                  |
| value          | `string \| number \| readonly string[] \| undefined` |                            |                                                                  |
| data-testid    | `string \| undefined`                                |                            |                                                                  |

## Explorer

