> Markdown version of [Message List](https://vaadin.com/docs/next/components/message-list). Section index: [llms.txt](https://vaadin.com/docs/next/components/llms.txt)

# Message List

Message List allows you to show a list of messages, for example, a chat log. You can configure the text content, information about the sender, and the time of sending for each message.

**Lit** — `message-list-component.ts`

```html
<vaadin-message-list
  .items="${[
    {
      text: 'Linsey, could you check if the details with the order are okay?',
      time: this.yesterday,
      userName: 'Matt Mambo',
      userColorIndex: 1,
    },
    {
      text: 'All good. Ship it.',
      time: this.fiftyMinutesAgo,
      userName: 'Linsey Listy',
      userColorIndex: 2,
      userImg: this.person ? this.person.pictureUrl : undefined,
    },
  ]}"
></vaadin-message-list>
```

**Flow** — `MessageListComponent.java`

```java
Person person = DataService.getPeople(1).get(0);
MessageList list = new MessageList();
Instant yesterday = LocalDateTime.now().minusDays(1)
        .toInstant(ZoneOffset.UTC);
Instant fiftyMinsAgo = LocalDateTime.now().minusMinutes(50)
        .toInstant(ZoneOffset.UTC);
MessageListItem message1 = new MessageListItem(
        "Linsey, could you check if the details with the order are okay?",
        yesterday, "Matt Mambo");
message1.setUserColorIndex(1);
MessageListItem message2 = new MessageListItem("All good. Ship it.",
        fiftyMinsAgo, "Linsey Listy", person.getPictureUrl());
message2.setUserColorIndex(2);
list.setItems(Arrays.asList(message1, message2));
add(list);
```

**React** — `message-list-component.tsx`

```tsx
const person = useSignal<Person | undefined>(undefined);

useEffect(() => {
  getPeople({ count: 1 }).then(({ people }) => {
    person.value = people[0];
  });
}, []);

const isoMinutes = 'yyyy-MM-dd HH:mm';
const yesterday = format(subDays(new Date(), 1), isoMinutes);
const fiftyMinutesAgo = format(subMinutes(new Date(), 50), isoMinutes);

const items = [
  {
    text: 'Linsey, could you check if the details with the order are okay?',
    time: yesterday,
    userName: 'Matt Mambo',
    userColorIndex: 1,
  },
  {
    text: 'All good. Ship it.',
    time: fiftyMinutesAgo,
    userName: 'Linsey Listy',
    userColorIndex: 2,
    userImg: person.value?.pictureUrl,
  },
];

return <MessageList items={items} />;
```

The messages in the list can be populated with the `items` property. The `items` property is of type `Array`, with JSON objects in it. Each JSON object is a single message.

## <a id="markdown"></a>Markdown

Use Markdown formatting in Message List to display rich text instead of plain text.

Markdown syntax gives you rich text formatting with headings, lists, links, and more. You can also use HTML tags within Markdown content. Message List automatically sanitizes content to prevent rendering dangerous HTML.

**Lit** — `message-list-markdown.ts`

```html
@state()
  private items: MessageListItem[] = [
    {
      text: '**Hello team!** Did everyone review the *design document* for the new project?',
      userName: 'Alex Johnson',
    },
    {
      text: `## Project Update
I've completed the initial research phase and documented my findings.

* UI mockups ✅
* Market analysis ✅
* [See detailed report](https://vaadin.com)

Let me know your thoughts!`,
      userName: 'Sam Rivera',
    },
  ];

  protected override render() {
    return html`<vaadin-message-list .items="${this.items}" markdown></vaadin-message-list>`;
  }
```

**Flow** — `MessageListMarkdown.java`

```java
MessageList list = new MessageList();
list.setItems(new MessageListItem(
        "**Hello team!** Did everyone review the *design document* for the new project?",
        "Alex Johnson"),
        new MessageListItem(
                """
                        ## Project Update
                        I've completed the initial research phase and documented my findings.

                        * UI mockups ✅
                        * Market analysis ✅
                        * [See detailed report](https://vaadin.com)

                        Let me know your thoughts!
                        """,
                "Sam Rivera"));
list.setMarkdown(true);
```

**React** — `message-list-markdown.tsx`

```tsx
const items = useSignal<MessageListItem[]>([]);

  useEffect(() => {
    items.value = [
      {
        text: '**Hello team!** Did everyone review the *design document* for the new project?',
        userName: 'Alex Johnson',
      },
      {
        text: `## Project Update
I've completed the initial research phase and documented my findings.

* UI mockups ✅
* Market analysis ✅
* [See detailed report](https://vaadin.com)

Let me know your thoughts!`,
        userName: 'Sam Rivera',
      },
    ];
  }, []);

  return <MessageList items={items.value} markdown />;
```

## <a id="attachments"></a>Attachments (since V25.1)

Messages can include file attachments. Image attachments are displayed as clickable thumbnail previews, while other file types are shown with a file icon and name.

**Lit** — `message-list-attachments.ts`

```html
<vaadin-message-list
  @attachment-click="${(e: CustomEvent) => {
    this.statusText = 'Clicked: ' + e.detail.attachment.name;
  }}"
  .items="${[
    {
      text: 'Here are the documents for the project.',
      time: this.yesterday,
      userName: 'Matt Mambo',
      userColorIndex: 1,
      attachments: [
        {
          name: 'project-proposal.pdf',
          url: 'https://example.com/files/proposal.pdf',
          type: 'application/pdf',
        },
        {
          name: 'budget-overview.xlsx',
          url: 'https://example.com/files/budget.xlsx',
          type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        },
      ],
    },
    {
      text: 'Thanks! Here\u0027s a photo from the offsite.',
      time: this.fiftyMinutesAgo,
      userName: 'Linsey Listy',
      userColorIndex: 2,
      attachments: [
        {
          name: 'landscape.jpg',
          url: landscapeImage,
          type: 'image/jpeg',
        },
      ],
    },
  ]}"
></vaadin-message-list>
<span>${this.statusText}</span>
```

**Flow** — `MessageListAttachments.java`

```java
MessageListItem message1 = new MessageListItem(
        "Here are the documents for the project.", yesterday,
        "Matt Mambo");
message1.setUserColorIndex(1);

message1.addAttachment(new MessageListItem.Attachment(
        "project-proposal.pdf",
        "https://example.com/files/proposal.pdf", "application/pdf"));
list.setItems(Arrays.asList(message1, message2));

var status = new Span("Click an attachment to see its name here.");
list.addAttachmentClickListener(event -> {
    status.setText("Clicked: " + event.getAttachment().name());
});
```

**React** — `message-list-attachments.tsx`

```tsx
const isoMinutes = 'yyyy-MM-dd HH:mm';
const yesterday = format(subDays(new Date(), 1), isoMinutes);
const fiftyMinutesAgo = format(subMinutes(new Date(), 50), isoMinutes);

const items = [
  {
    text: 'Here are the documents for the project.',
    time: yesterday,
    userName: 'Matt Mambo',
    userColorIndex: 1,
    attachments: [
      {
        name: 'project-proposal.pdf',
        url: 'https://example.com/files/proposal.pdf',
        type: 'application/pdf',
      },
      {
        name: 'budget-overview.xlsx',
        url: 'https://example.com/files/budget.xlsx',
        type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
      },
    ],
  },
  {
    text: "Thanks! Here's a photo from the offsite.",
    time: fiftyMinutesAgo,
    userName: 'Linsey Listy',
    userColorIndex: 2,
    attachments: [
      {
        name: 'landscape.jpg',
        url: landscapeImage,
        type: 'image/jpeg',
      },
    ],
  },
];

const statusText = useSignal('Click an attachment to see its name here.');

return (
  <>
    <MessageList
      items={items}
      ref={(messageList) => {
        if (messageList) {
          messageList.addEventListener('attachment-click', (e: CustomEvent) => {
            statusText.value = 'Clicked: ' + e.detail.attachment.name;
          });
        }
      }}
      // Switch to using onAttachmentClick once https://github.com/vaadin/web-components/pull/11189 is available in a release.
      // onAttachmentClick={(e) => {
      //   statusText.value = 'Clicked: ' + e.detail.attachment.name;
      // }}
    />
    <span>{statusText.value}</span>
  </>
);
```

Each attachment has a name, a URL, and a MIME type. The MIME type determines how the attachment is rendered: types starting with `image/` are shown as thumbnail previews, while all other types are shown as file icons. You can listen for attachment click events to handle downloads or other actions.

## <a id="typing-indicator"></a>Typing Indicator Flow (since V25.3)

> **Note: Preview Feature**
>
> This is a preview version of the Message List typing indicator. You need to enable it with the [feature flag](https://vaadin.com/docs/next/flow/configuration/feature-flags.md) `com.vaadin.experimental.messageListTypingIndicator`. Preview versions may lack some planned features, and breaking changes may be introduced in any Vaadin version. We encourage you to try it out and [provide feedback in the Forum](https://vaadin.com/forum/) so that we can make it stable. You can report bugs and request additional features on [GitHub](https://github.com/vaadin/flow-components/issues/new/choose).

Message List can show which users are typing, for example to tell the participants of a chat that a reply is on its way. The typing indicator appears after the last message. By default, it shows the avatars and names of the typing users, and the text *Typing…*. If the list is scrolled to the end when the indicator appears, the list scrolls to keep the indicator in view.

`MessageListTypingIndicator.java`

```java
MessageList list = new MessageList(message);

MessageListUser linsey = new MessageListUser("Linsey Listy",
        person.getPictureUrl());
linsey.setColorIndex(2);
MessageListUser sam = new MessageListUser("Sam Swanson");
sam.setColorIndex(3);
list.setTypingUsers(linsey, sam);

// Change the type of the typing indicator when the option changes
typeGroup.addValueChangeListener(
        event -> list.setTypingIndicatorType(event.getValue()));
```

Set the typing users with `setTypingUsers()`. Each call replaces the current typing users, and a call without users hides the indicator. Message List doesn’t remove the users on its own, so update them when someone stops typing or sends a message. `getTypingUsers()` returns the current users, for example to filter out the one who stopped.

Without the feature flag, the methods that configure the typing indicator throw a `MessageListTypingIndicatorExperimentalFeatureException`, at the latest when the list is attached. The `aiComponents` flag of the [AI support features](https://vaadin.com/docs/next/flow/ai-support.md) enables the typing indicator as well.

Each typing user is a `MessageListUser`. Like the sender of a message, it has a name, an abbreviation, an image, and a color index for the avatar, and class names added with `addClassNames()` apply to the avatar. Changing any of these updates the indicator right away. The image is either a URL set with `setImage()`, or a `DownloadHandler` set with `setImageHandler()`, which Message List serves as a resource. Since the resource is registered for the list that the user was last added to, use a separate `MessageListUser` instance for each list.

Message List hides the typing indicator from assistive technologies and keeps it out of the focus order. Instead, it announces the names of the typing users and the typing indicator text through a live region.

### <a id="indicator-types"></a>Indicator Types

Set the type of the typing indicator with `setTypingIndicatorType()`, passing one of the `MessageListTypingIndicatorType` constants. You can change the type at any time, even while the indicator is visible.

| Type       | Description                                                                                                                                                    |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DEFAULT`  | Shows the indicator like a message: the avatars and names of the typing users, and the typing indicator text as the content.                                   |
| `ELLIPSIS` | Shows the avatars of the typing users and an animated ellipsis. The names and the typing indicator text aren’t shown, but screen readers still announce them.  |
| `MINIMAL`  | Shows the indicator as a single compact row: the avatars, followed by the names of the typing users and the typing indicator text, which appears in lowercase. |

### <a id="typing-indicator-text"></a>Typing Indicator Text

Replace the default text *Typing…*, for example with a translation, by setting a `MessageListI18n` object:

```java
list.setI18n(new MessageListI18n()
        .setTypingIndicatorText("Escribiendo…"));
```

The names of the typing users are joined into a list in the language of the page, so the text is the only part that needs a translation. The screen reader announcement and the `MINIMAL` type put the text right after the names, so choose a text that reads well there.

### <a id="binding-to-a-signal"></a>Binding to a Signal

Use `bindTypingUsers()` to bind the typing users to a [signal](https://vaadin.com/docs/next/flow/ui-state.md). The indicator then follows the signal: it updates whenever users are added to or removed from the list signal, and whenever the value of an individual user signal changes. While a binding is active, calling `setTypingUsers()` or binding another signal throws a `BindingActiveException`. The indicator follows the signal only while the list is attached.

```java
ListSignal<MessageListUser> typingUsers = new ListSignal<>();
list.bindTypingUsers(typingUsers);

// Linsey starts typing
ValueSignal<MessageListUser> linseyTyping = typingUsers.insertLast(linsey);

// Linsey stops typing
typingUsers.remove(linseyTyping);
```

## <a id="usage-for-ai-chats"></a>Usage for AI Chats

For a higher-level approach that eliminates boilerplate wiring, see the [AI support features](https://vaadin.com/docs/next/flow/ai-support.md), which connect Message List and Message Input to an LLM provider automatically.

Combine Message List with Message Input to create effective AI chat interfaces. Build your AI chat interface with:

- Message List with Markdown formatting to display conversation history

- Message Input for user interactions

- Backend service to handle AI interactions

Follow these best practices for a smooth user experience:

- Append tokens to assistant messages in real time as they arrive

- Distinguish the user’s prompts from the assistant’s responses, for example with the [style variants for one-to-one conversations](https://vaadin.com/docs/next/components/message-list/styling.md#one-to-one-conversations)

- Show visual indicators for system activity

- Preserve conversation history by loading past messages and maintaining session state

**Lit** — `message-list-ai-chat.ts`

```html
<vaadin-message-list .items="${this.messageListItems}" markdown></vaadin-message-list>
```

**Lit** — `LLMChatService.java`

```java
@BrowserCallable
@AnonymousAllowed
public class LLMChatService {

    public List<Message> getHistory(String chatId) {
        // Implement your message history retrieval logic here
        return LLMClient.getHistory(chatId);
    }

    public Flux<String> stream(String chatId, String userMessage) {
        // Implement your message streaming logic here
        return LLMClient.stream(chatId, userMessage);
    }
}
```

**Flow** — `MessageListAiChat.java`

```java
MessageList list = new MessageList();
list.setMarkdown(true);
```

**React** — `message-list-ai-chat.tsx`

```tsx
<MessageList items={messageListItems.value} markdown />
```

**React** — `LLMChatService.java`

```java
@BrowserCallable
@AnonymousAllowed
public class LLMChatService {

    public List<Message> getHistory(String chatId) {
        // Implement your message history retrieval logic here
        return LLMClient.getHistory(chatId);
    }

    public Flux<String> stream(String chatId, String userMessage) {
        // Implement your message streaming logic here
        return LLMClient.stream(chatId, userMessage);
    }
}
```

`screen-reader-only.css`

```css
.screen-reader-only {
  border-width: 0;
  clip: rect(0, 0, 0, 0);
  height: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
  white-space: nowrap;
  width: 1px;
}
```

## <a id="related-components"></a>Related Components

| Component                                                                 | Usage recommendations                                |
| ------------------------------------------------------------------------- | ---------------------------------------------------- |
| [Message Input](https://vaadin.com/docs/next/components/message-input.md) | Allow users to author and send messages.             |
| [Markdown](https://vaadin.com/docs/next/components/markdown.md)           | Standalone component for rendering Markdown content. |

`EE10DB8E-479C-41D0-9A77-BFA41C340BFB`
