
Message 1 of 20
Keep new messages visible with auto-scroll, scroll-to-bottom affordance, and automatic history loading on scroll.
Try the scroll-to-bottom affordance live: scroll up inside the preview to reveal the jump-to-latest button, and try each scrollBehavior.

Message 1 of 20
Message 2 of 20

Message 3 of 20
Message 4 of 20

Message 5 of 20
Message 6 of 20

Message 7 of 20
Message 8 of 20

Message 9 of 20
Message 10 of 20

Message 11 of 20
Message 12 of 20

Message 13 of 20
Message 14 of 20

Message 15 of 20
Message 16 of 20

Message 17 of 20
Message 18 of 20

Message 19 of 20
Message 20 of 20
The message list automatically manages scroll position so new messages and streaming content stay visible without user intervention. Users can still scroll up to read earlier messages without losing their place.
The message list automatically scrolls to the bottom when:
The auto-scroll behavior is gated by a buffer—if the user has scrolled more than buffer pixels away from the bottom, automatic scrolling pauses so the user can read earlier messages without interruption.
The same buffer defines the bottom zone for the message list's onReachBottom callback, which fires once each time the viewport enters it.
Control auto-scrolling through the features prop on ChatBox.
The default buffer is 150px:
// Default — auto-scroll on with a 150px buffer
<ChatBox adapter={adapter} />
// Custom 300px buffer threshold
<ChatBox adapter={adapter} features={{ autoScroll: { buffer: 300 } }} />
// Disable auto-scroll entirely
<ChatBox adapter={adapter} features={{ autoScroll: false }} />
When auto-scroll is disabled, users can still scroll to the bottom manually using the scroll-to-bottom affordance.
Send a message, then scroll up while the long reply streams in — auto-scroll pauses once you're more than buffer pixels from the bottom and resumes when you scroll back within it.
A floating button appears when the user scrolls away from the bottom. Clicking it smoothly scrolls back to the latest message. Try it in the interactive playground at the top of this page.
The affordance is enabled by default. Disable it with:
<ChatBox adapter={adapter} features={{ scrollToBottom: false }} />
The affordance also supports an unseen-message count badge and an aria-label that includes the unseen count when present.
The affordance's accessible name comes from the scrollToBottomLabel and scrollToBottomWithCountLabel locale strings — see Localization to translate them.
The message list also renders a visually hidden role="status" live region (the messageListStatus slot) that announces when a streamed response starts and completes to screen readers.
For the full keyboard and screen-reader model, see the Accessibility page and Message list—Accessibility.
The ChatMessageList component exposes a ref handle for programmatic scroll control:
import { ChatMessageList } from '@mui/x-chat';
import type { MessageListRootHandle } from '@mui/x-chat/headless';
const listRef = React.useRef<MessageListRootHandle>(null);
// Scroll to bottom programmatically
listRef.current?.scrollToBottom({ behavior: 'smooth' });
<ChatMessageList ref={listRef} />;
Child components inside the message list can access scroll state via context:
import { useMessageListContext } from '@mui/x-chat/headless';
function CustomScrollIndicator() {
const { isAtBottom, unseenMessageCount, scrollToBottom } = useMessageListContext();
if (isAtBottom) return null;
return (
<button onClick={() => scrollToBottom({ behavior: 'smooth' })}>
{unseenMessageCount} new messages
</button>
);
}
| Property | Type | Description |
|---|---|---|
isAtBottom |
boolean |
Whether the scroll position is at the bottom |
unseenMessageCount |
number |
Messages added since the user scrolled away |
scrollToBottom |
(options?) => void |
Scroll to the latest message |
When the user scrolls to the top of the message list, older messages are loaded automatically via the adapter's listMessages method.
The message list preserves the current scroll position during prepend so the user doesn't lose their place.
The adapter signals whether more history is available through the hasMore flag:
async listMessages({ conversationId, cursor }) {
const res = await fetch(`/api/conversations/${conversationId}/messages?cursor=${cursor ?? ''}`);
const { messages, nextCursor, hasMore } = await res.json();
return { messages, cursor: nextCursor, hasMore };
},
When hasMore is true, the message list continues to load older messages as the user scrolls up.
When hasMore is false, no more history is requested when the user reaches the top.
You can also check the history loading state programmatically:
const { hasMoreHistory, loadMoreHistory } = useChat();
// Trigger manually if needed
await loadMoreHistory();
listMessages method that powers history loading.See the documentation below for a complete reference to all of the props and classes available to the components mentioned here.