Message list UI
The message list is the scrolling part of a channel or a thread. It renders what a message paginator holds and turns the user's scrolling into paginator calls. This page covers the two lists, what they show after a jump, the separators, and how one message is put together. The props are in the MessageList and VirtualizedMessageList references.

MessageList and VirtualizedMessageList
Both lists render the messages of the channel or thread they are in:
- Inside a
Thread, they render the thread's replies,thread.messagePaginator, with the parent message above them. - Otherwise they render the channel's messages,
channel.messagePaginator.
So the same <MessageList /> works in both places. Each list subscribes to its paginator's state, and:
- loads older messages when the user scrolls to the top;
- loads newer messages when the user scrolls to the bottom of an interval that isn't the latest one;
- marks the channel or thread read while the user is at the latest message and the page is visible. Read state, unread and receipts covers the exceptions.
How the paginator holds intervals of messages is on Message paginators.
The two lists differ in how they render:
MessageList |
VirtualizedMessageList |
|
|---|---|---|
| Renders | Every loaded message | Only the messages near the visible area, with react-virtuoso |
| Date separators | On; withDateSeparator turns them off |
Off; withDateSeparator turns them on |
| Grouping by sender | On; noGroupByUser turns it off |
Off; shouldGroupByUser turns it on |
| Typing indicator | Built in | Only with a TypingIndicator override |
| Message UI | The MessageUI override |
The VirtualMessage override, else MessageUI |
Use MessageList unless the DOM gets too large: very long histories, or channels with a lot of traffic such as livestreams. VirtualizedMessageList keeps the DOM small, and takes react-virtuoso options through additionalVirtuosoProps.
Jumps on screen
A jump moves the list to one message: a quoted message, a search result, a pinned message, the first unread message or the latest one. The paginator loads the interval of messages around the target, and the list then shows that interval:
- The interval replaces what was on screen, and the list scrolls the target to the middle.
- The target is highlighted with the
str-chat__message--highlightedclass.MessageListremoves the highlight three seconds after the message has been on screen. - If the interval isn't the latest one, the scroll-to-latest button shows, and scrolling down loads newer messages.
The jump itself is on Message paginators, and every place that starts one is on Moving between channels, threads and messages.
Three floating elements tell the user there is more to see:
- The unread notification,
UnreadMessagesNotification, sits at the top of a channel's list. It shows "5 unread" while the unread separator is above the visible area or not loaded at all. Clicking it jumps to the first unread message. Its ✕ marks the channel read. WithshowUnreadNotificationAlways, it also shows while the separator is below the visible area. Threads don't show it. - The scroll-to-latest button,
ScrollToLatestMessageButton, shows while the list isn't at the latest message. Its badge counts the messages that arrived while the user was scrolled up, or in a thread, the new replies. Clicking it scrolls to the bottom, or jumps to the latest message when it isn't loaded. - The new message notification,
NewMessageNotification, shows while the user is scrolled up and new messages arrive, and while the list shows an older interval. It reads "New Messages!", or "3 new messages" when the unread snapshot counts unread messages.
Separators
Date separators mark where a day starts. A DateSeparator goes before the first message and between messages sent on different days. While the user scrolls, a floating DateSeparator at the top shows the date of the messages there. In a thread, the parent message has a date separator of its own. formatDate changes how the date is written; Localization covers date formats.
With date separators on, the list also adds one before the first message that arrived since the user last read the channel, unless the user sent it. hideNewMessageSeparator turns that one off.
The unread separator, UnreadMessagesSeparator, marks the first unread message in a channel, with "5 unread". It renders from the paginator's unread snapshot: the unread state when the user opened the channel. Opening the channel marks it read but keeps the snapshot, so the user can still see where the new messages start. The snapshot clears when the user, at the latest message, catches up on a new one, and when the user clicks the separator's ✕, which also marks the channel read. Threads don't show it. Read state and unread counts explains the snapshot.
A message
For each message the list renders Message. Message creates the message's handlers (react, delete, flag, mute, pin, open the thread, retry a failed send) and puts them in MessageContext with the message. Then it renders the MessageUI override, by default MessageUI, with no props. A message UI reads everything from useMessageContext().
The default MessageUI is made of parts, each its own override key:
| Part | Override key |
|---|---|
| The sender's avatar | Avatar |
| The text | Rendered by renderText, below |
| Attachments, including a shared location | Attachment |
| A quoted message | QuotedMessage |
| Reactions | MessageReactions |
| Status: sending, sent, delivered or read | MessageStatus |
| The actions menu | MessageActions |
| The timestamp and the edited mark | MessageTimestamp, MessageEditedIndicator |
| The replies button, in a channel | MessageRepliesCountButton |
| Pinned, reminder, translated and also-sent-in-channel marks | PinIndicator, ReminderNotification, MessageTranslationIndicator, MessageAlsoSentInChannelIndicator |
| A deleted or blocked message | MessageDeleted (no default), MessageDeletedBubble, MessageBlocked |
A poll renders in the message's bubble. A system message, such as "Cleo joined the channel", renders the MessageSystem override instead of a message UI. The text of a message that Chat's isMessageAIGenerated marks as AI-generated renders with StreamedMessageText, which shows it as it streams in.
renderText
The text goes through renderText(text, mentionedUsers, options). The default renders Markdown with react-markdown, and adds mentions and emoji. Pass your own as the list's renderText prop. This one keeps the default and renders Markdown headings as bold text:
import type { RenderTextFunction } from "stream-chat-react";
import { MessageList, renderText } from "stream-chat-react";
const renderTextWithoutHeadings: RenderTextFunction = (
text,
mentionedUsers,
options,
) =>
renderText(text, mentionedUsers, {
...options,
customMarkDownRenderers: {
...options?.customMarkDownRenderers,
h1: ({ children }) => <strong>{children}</strong>,
h2: ({ children }) => <strong>{children}</strong>,
},
});
export const ChannelMessages = () => (
<MessageList renderText={renderTextWithoutHeadings} />
);Keep the options the list passes in: they carry the mentions to render. The options are in the renderText reference.
Replacing parts
Register replacements with WithComponents. Overriding components explains how overrides apply.
| Override key | Replaces |
|---|---|
MessageUI |
A whole message. Gets no props; reads useMessageContext(). |
VirtualMessage |
A whole message in VirtualizedMessageList |
| The keys in A message | One part of a message |
MessageSystem |
A system message |
DateSeparator |
The date separators, floating one included, but not a thread parent's |
UnreadMessagesSeparator |
The unread separator |
UnreadMessagesNotification, NewMessageNotification |
The floating notifications |
TypingIndicator |
The typing indicator |
EmptyStateIndicator |
What a channel with no messages shows |
LoadingIndicator |
The spinner above the messages: in MessageList while an older page loads, in VirtualizedMessageList while any page loads |
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 |
The default ThreadHead isn't exported, so a replacement for it can't render the default inside it.
This list shows each message as one compact line:
import {
MessageList,
MessageText,
MessageTimestamp,
useMessageContext,
WithComponents,
} from "stream-chat-react";
const CompactMessage = () => {
const { message } = useMessageContext();
return (
<div className="compact-message">
<MessageTimestamp />
<strong>{message.user?.name ?? message.user?.id}</strong>
<MessageText />
</div>
);
};
const overrides = { MessageUI: CompactMessage };
export const CompactMessages = () => (
<WithComponents overrides={overrides}>
<MessageList />
</WithComponents>
);MessageText and MessageTimestamp read the message from MessageContext too, so they need no props. The Custom message recipe builds a complete message UI.