Message list UI
The message list is the scrolling part of a channel or a thread. It shows the messages and loads more as the user scrolls. Use this page to choose between the SDK's two message lists, to learn what they show besides the messages, such as separators and notifications, and to customize them. How one message is put together is on Message UI. The props are in the MessageList and VirtualizedMessageList references.

This is the default message list of a channel, scrolled up after three new messages arrived. Its parts:
| Part | Explained in |
|---|---|
| 1. The floating date separator | Separators |
| 2. A date separator | Separators |
| 3. The unread messages separator | Separators |
| 4. A message group: Bob's messages in a row | Message grouping |
| 5. The new message notification | Floating notifications |
| 6. The scroll-to-latest button | Floating notifications |
The typing indicator isn't in the image: it shows only while the list is scrolled to the bottom, and the notification and the button only while it isn't.
MessageList and VirtualizedMessageList
Both lists show the channel's messages, or a thread's replies when rendered inside a <Thread/>. They differ in what they put in the page:
<MessageList/> |
<VirtualizedMessageList/> |
|
|---|---|---|
| Renders | every message of the active interval | only the messages near the visible area, with react-virtuoso |
| Suits | most channels and threads | very long histories, or a lot of traffic, such as a livestream |
| Date separators, grouping and typing indicator | on by default | off by default |
Other differences, such as in the floating notifications and jumps, are in Differences between the lists.
Loading messages
The list shows the messages that channel.messagePaginator or thread.messagePaginator loaded. It shows one interval at a time, the active one: usually the latest messages, or those around a message the user jumped to.
- Scrolling to the top loads older messages, and scrolling to the bottom of an older interval loads newer ones.
- At the latest message, with the page visible, the list marks the channel or thread read. Read state, unread and receipts covers the exceptions.
Separators
Separators mark a point in the conversation. The list renders four kinds:
| Separator | Component | Marks | Hidden by |
|---|---|---|---|
| Date separator | <DateSeparator/> |
where a day starts | withDateSeparator={false}. It defaults to true in <MessageList/> and false in <VirtualizedMessageList/>. |
| Floating date separator | <DateSeparator/> |
the date of the messages at the top | withDateSeparator={false} |
| Thread parent's date separator | <DateSeparator/> |
the date of the parent message, above a thread's replies | nothing |
| Unread messages separator | <UnreadMessagesSeparator/> in the list, and <UnreadMessagesNotification/> floating at the top while it's out of view |
the first unread message in a channel: "5 unread" in the list, and "↑ 5 unread" floating, which jumps to it. Not shown in a thread. | the user clicking the ✕ on either, which also marks the channel read |





Before the first unread message, the list can also add a date separator, even within the same day. hideNewMessageSeparator removes it.
To replace a separator, register your own component under the key DateSeparator, UnreadMessagesSeparator or UnreadMessagesNotification in <WithComponents/>. The DateSeparator override also replaces the date above a thread's parent message.
The floating date separator shows while the user scrolls, and hides a second after the scrolling stops. It appears only once the date separator of the day at the top has scrolled out of view, so the same date is never shown twice. The lists find the date separators on screen by their str-chat__date-separator class and read each one's date from its data-date attribute, to decide when the floating one shows. A DateSeparator replacement keeps both, and adds the str-chat__date-separator--floating class when it gets floating, as the DateSeparator reference shows. While a date separator sits where the floating one is drawn, the floating one stays hidden, so it never covers it.
To change how a date separator writes its date, pass a formatDate function to <MessageList/> or <VirtualizedMessageList/>, which takes the Date and returns the text to show, such as "Mon, 12 May". It applies to the date separators between messages in <MessageList/>, and in both lists to the one above a thread's parent message. It doesn't apply to the floating one, or to those between messages in <VirtualizedMessageList/>. Localization covers date formats.
The unread messages separator keeps its place after opening the channel marks it read, so the user still sees where the new messages start. The paginator keeps that place in its unread snapshot. It goes away when the user catches up at the latest message, or clicks its ✕. Live view and unread on Message paginators explains the snapshot.
Message grouping
Messages a sender posts one after another form a group: their avatar and name show once, and the bubbles sit closer together. A group also ends:
- at a date separator, so each day starts a new group;
- at a system or error message;
- at a message with attachments, which always stands alone;
- before a message with reactions, which starts a new group;
- after an edited message.
Grouping is on by default in <MessageList/> and off in <VirtualizedMessageList/>. The groupByUser prop of either list turns it on or off.
Each message gets its place in its group: top, middle, bottom or single. The message UI styles the message from it.
Both lists take two props to change it:
maxTimeBetweenGroupedMessagesalso starts a new group when two messages are further apart than this, in milliseconds. It isn't set by default.getGroupStylesreplaces the function that decides each message's place in its group. It receives one object with the message and its neighbors.
Here is a getGroupStyles function that keeps the default grouping, but shows pinned messages on their own:
import { getGroupStyles, isLocalMessage, MessageList } from "stream-chat-react";
// Pinned messages stand alone; every other message is grouped as by default.
const pinnedStandAlone: typeof getGroupStyles = (params) =>
isLocalMessage(params.message) && params.message.pinned
? "single"
: getGroupStyles(params);
export const ChannelMessages = () => (
<MessageList getGroupStyles={pinnedStandAlone} />
);Floating notifications
Three elements float over the list to tell the user there is more to see:
| Element | Shows | When | Clicking it | Component to override (<WithComponents/> key) |
|---|---|---|---|---|
| Floating unread messages separator | "↑ 5 unread", at the top of a channel's list; see Separators. Not shown in a thread. | while the first unread message is above the visible area, or not loaded | jumps to the first unread message. Its ✕ marks the channel read. | UnreadMessagesNotification |
| Scroll-to-latest button | an arrow, with a badge | while the list isn't at the latest message | scrolls to the bottom, or loads the latest messages first | ScrollToLatestMessageButton |
| New message notification | "3 new messages", or "New Messages!" | while the user is scrolled up and someone else's message arrives | nothing: it's a status label, read out by screen readers | NewMessageNotification |



- The floating unread messages separator also shows while the first unread message is below the visible area, with
showUnreadNotificationAlways. - The scroll-to-latest badge counts different things in a channel and in a thread. In a channel, it's the user's unread count,
channel.state.read[userId].unread_messages, so it includes messages from before the user opened the channel, and drops to 0 once the channel is read. In a thread, it's the replies posted while the user was scrolled up. - The new message notification counts the unread messages from the unread snapshot. Without them, it says "New Messages!".
Jumps on screen
A jump moves the list to one message, such as a quoted message, a search result, a pinned message, or the first unread one. Moving between channels, threads and messages lists every place in the UI that starts a jump.
On a jump, the list:
- shows the messages around the target, loading them first if needed, with the target in the middle;
- highlights the target with the
str-chat__message--highlightedclass.<MessageList/>removes it after 3 seconds in view (the jump'sfocusSignalTtlMs).<VirtualizedMessageList/>doesn't remove it: it stays until the next jump; - shows the scroll-to-latest button if the target isn't among the latest messages.
How the paginator jumps is on Message paginators.
Scrolling
Both lists follow new messages: they scroll down when the user sends a message, or when someone else does while the user is at the bottom. They don't follow while the user reads older messages after a jump.
| Prop | Lists | What it does |
|---|---|---|
suppressAutoscroll |
both | Turns following new messages off. |
scrolledUpThreshold |
<MessageList/> |
How far from the bottom, in pixels, counts as scrolled up. 200 by default. |
stickToBottomScrollBehavior |
<VirtualizedMessageList/> |
How the list scrolls when it follows the user's own message: 'smooth' by default. |
scrollToLatestMessageOnFocus |
<VirtualizedMessageList/> |
Scrolls to the latest message when the page gets focus. Off by default. |
Differences between the lists
The sections above describe each part. This table puts side by side how the parts differ between the two lists, in a channel and in a thread:
<MessageList/> in a channel |
<MessageList/> in a thread |
<VirtualizedMessageList/> in a channel |
<VirtualizedMessageList/> in a thread |
|
|---|---|---|---|---|
| Parent message above the list | no | yes | no | yes |
| Date separators | on | on, and the parent message has one of its own | off | off, but the parent message has one of its own |
| Grouping | on | on | off | off |
| Unread messages separator, in the list and floating | yes | no | yes | no |
| Scroll-to-latest badge counts | the user's unread messages | replies posted while scrolled up | the user's unread messages | replies posted while scrolled up |
| Highlight after a jump | removed after 3 s | removed after 3 s | stays until the next jump | stays until the next jump |
| Typing indicator | built in | built in, for typing in the thread | only with a TypingIndicator override |
only with a TypingIndicator override, for typing in the thread |
| Empty state | EmptyStateIndicator |
nothing | EmptyStateIndicator |
EmptyStateIndicator, which draws nothing by default |
| Loading indicator | while older messages load | while older messages load | while any messages load | while any messages load |
| Read and delivered marks | on the user's last message, or on every own message with returnAllReadData |
none: only "sending" shows | as in <MessageList/> |
none: only "sending" shows |
| Giphy preview before sending | in the list | in the list | in the list, or after it with separateGiphyPreview |
in the list, or after it with separateGiphyPreview |
Two differences need a sentence more:
- The floating unread messages separator decides when to show in a different way.
<MessageList/>watches where the separator in the list is.<VirtualizedMessageList/>renders only part of the list, so it compares the dates of the messages on screen with the user's last read. - Marking read in
<VirtualizedMessageList/>also waits until the last message is fully visible.
Changing what the list renders
| To… | Prop | Lists |
|---|---|---|
| leave deleted messages out | hideDeletedMessages |
both |
| change, replace or add items after the list has built them, for example a separator of your own | reviewProcessedMessage |
both |
| render the items yourself | renderMessages |
<MessageList/> |
| render each item yourself | customMessageRenderer |
<VirtualizedMessageList/> |
Replacing parts
Register replacements with <WithComponents/>. Overriding components explains how overrides apply.
Component to override (<WithComponents/> key) |
Replaces |
|---|---|
DateSeparator |
The date and new messages separators, the floating one and the date above a thread's parent message included |
UnreadMessagesSeparator |
The unread messages separator in the list |
UnreadMessagesNotification, ScrollToLatestMessageButton, NewMessageNotification |
The floating notifications |
TypingIndicator |
The typing indicator |
EmptyStateIndicator |
What an empty list shows |
LoadingIndicator |
The spinner at the top of the list while messages load: older messages in <MessageList/>, any messages in <VirtualizedMessageList/> |
ThreadHead, ThreadStart |
The parent message above a thread's replies, and the reply count under it |
MessageListWrapper, MessageListItem |
The ul around the messages and the li around each, in <MessageList/>. They get MessageListWrapperProps and MessageListItemProps; spread them onto the element, because jump-to-message and scroll restoration find items by data-message-id |
The keys for a message and its parts, MessageUI and VirtualMessage included, are on Message UI.
The default ThreadHead isn't exported, so a replacement for it can't render the default inside it.