Message Scroller
A Message Scroller allows users to scroll a conversation, which opens at the newest message and moves to each new turn.
When to Use
- for a conversation that is taller than its panel and grows while it is open
- to open a saved conversation at its newest message or, with
defaultScrollPosition="last-anchor", at its last turn - when a reply arrives in pieces and the viewport should follow it, with
autoScroll - to jump to one message from a search result or a link
When Not to Use
- for any other list that is longer than its panel (use a Scroll Area)
- for a few messages that always fit (stack them in a
div)
A Message Scroller is a client component, and its viewport adds a stop to the tab order.
Anatomy
| Element | Usage |
|---|---|
| Viewport | Required |
| Item | Required |
| Scroll button | Optional |
MessageScrollerProvider keeps the scroll state and renders no element. MessageScroller is the frame, MessageScrollerViewport is the element that scrolls, and MessageScrollerContent stacks the rows. Place every row in a MessageScrollerItem, whether it contains a Message, a Marker, or a Marker rendered as a button that loads earlier messages.
See Message for the place of the Message Scroller among the other parts of a conversation.
Behavior
MessageScroller fills its parent in both directions, so set the height on the parent.
Opening Position
To set where the conversation opens, pass defaultScrollPosition to the provider. The value is read once, when the first rows mount.
| Value | Result |
|---|---|
'end' | The newest message, at the bottom of the viewport. This is the default. |
'start' | The first message |
'last-anchor' | The last row with scrollAnchor, near the top of the viewport. It falls back to 'end' when no row is an anchor or when everything from that row down fits in the viewport. |
New Turns
A row with scrollAnchor starts a turn. In the preview, each message that the current user sends has scrollAnchor. When such a row is added at the end, the viewport scrolls it near the top, and the end of the previous row stays in view. The rest of the viewport is free for the reply. The viewport scrolls in this way wherever users have scrolled to.
If autoScroll isn’t set, a row that has no scrollAnchor doesn’t scroll the viewport. The row is added below the viewport’s bottom edge, and the scroll button appears.
Following a Reply
If autoScroll is set on the provider, the end stays in view while rows without an anchor arrive or the last row grows. The viewport follows only while users are at the end. When users scroll with the wheel, by touch, or with a key, or jump to a message, it stops following. When they return to the end, by scrolling or with the scroll button, it follows again.
Earlier Messages
By default, rows that are added above the first one don’t move the conversation because the viewport keeps the first visible row in place. To turn this off, set preserveScrollOnPrepend={false} on the viewport. Set a stable messageId on every row so that the Message Scroller can find the row again.
Scroll Button
MessageScrollerButton appears while more than 8px of the conversation is below the viewport, and when users click it, the viewport scrolls there. At direction="start", it is positioned at the top and scrolls to the first message. While there is nothing to scroll to, it is inert and out of the tab order.
The scroll is smooth. When users request reduced motion, the viewport jumps instead. To make it jump for everyone, set behavior="auto" on the button.
Long Conversations
MessageScrollerItem sets content-visibility: auto, so the browser skips the rendering of a row that is off screen. The row stays in the markup, so users can still reach it with the browser’s find and with a screen reader.
Content
| String | Default | Source |
|---|---|---|
| Name of the scroll button | “Scroll to end”, “Scroll to start” | labels.messageScroller in the Summit Provider |
| Name of the viewport | “Messages” | labels.messageScroller in the Summit Provider |
Accessibility
The viewport is a named region that is focusable, so keyboard users can scroll the conversation with the browser’s scrolling keys. The content is a log, so screen readers announce each row as it is added.
Set aria-busy on MessageScrollerContent while a reply is still arriving, so that screen readers wait for the finished row. See the shadcn Message Scroller documentation for the reason.
Installation
npx shadcn@latest add @summit/message-scrollerThe CLI also adds @summit/button and @summit/summit-provider.
Usage
The Message Scroller doesn’t store messages. The product keeps the list, renders a row for each entry, and adds the new ones.
import { Bubble, BubbleContent } from '@/components/ui/bubble';
import { Message, MessageContent } from '@/components/ui/message';
import {
MessageScroller,
MessageScrollerButton,
MessageScrollerContent,
MessageScrollerItem,
MessageScrollerProvider,
MessageScrollerViewport,
} from '@/components/ui/message-scroller';
<div className="h-80">
<MessageScrollerProvider>
<MessageScroller>
<MessageScrollerViewport>
<MessageScrollerContent className="p-4">
{messages.map((message) => (
<MessageScrollerItem key={message.id} messageId={message.id} scrollAnchor={message.mine}>
<Message align={message.mine ? 'end' : 'start'}>
<MessageContent>
<Bubble variant={message.mine ? 'default' : 'secondary'}>
<BubbleContent>{message.text}</BubbleContent>
</Bubble>
</MessageContent>
</Message>
</MessageScrollerItem>
))}
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>
</div>;Follow a Reply
Set autoScroll on the provider.
<MessageScrollerProvider autoScroll>
<MessageScroller />
</MessageScrollerProvider>Open at the Last Turn
Set defaultScrollPosition="last-anchor".
<MessageScrollerProvider defaultScrollPosition="last-anchor">
<MessageScroller />
</MessageScrollerProvider>Jump to a Message
useMessageScroller returns scrollToMessage, scrollToEnd, and scrollToStart. You can call it in any component inside the provider, so the control can be placed outside the frame.
import { Button } from '@/components/ui/button';
import { useMessageScroller } from '@/components/ui/message-scroller';
function JumpToEstimate() {
const { scrollToMessage } = useMessageScroller();
return (
<Button variant="outline" size="sm" onClick={() => scrollToMessage('m4', { align: 'start' })}>
The estimate
</Button>
);
}Read the Scroll State
useMessageScrollerScrollable returns start and end, which are true while content extends past the matching edge.
import { useMessageScrollerScrollable } from '@/components/ui/message-scroller';
function EarlierMessagesHint() {
const { start } = useMessageScrollerScrollable();
return start ? <p>There are earlier messages.</p> : null;
}Scroll to the Start
Add a second button with direction="start".
<MessageScroller>
<MessageScrollerViewport />
<MessageScrollerButton direction="start" />
<MessageScrollerButton />
</MessageScroller>Name the Viewport
Pass aria-label to the viewport.
<MessageScrollerViewport aria-label="Conversation with Elise Martin" />API Reference
The parts accept the props of their matching parts in @shadcn/react, and the hooks are that library’s own. See the @shadcn/react Message Scroller documentation for scrollEdgeThreshold, scrollMargin, scrollPreviousItemPeek, and the options of scrollToMessage.
| Hook | Returns |
|---|---|
useMessageScroller | scrollToMessage, scrollToEnd, and scrollToStart |
useMessageScrollerScrollable | start and end, which say whether content extends past each edge |
useMessageScrollerVisibility | currentAnchorId and visibleMessageIds, for the rows that have a messageId |
MessageScrollerProvider
| Prop | Type | Default | Description |
|---|---|---|---|
autoScroll | boolean | false | Follows new content while users are at the end |
defaultScrollPosition | 'start' | 'end' | 'last-anchor' | 'end' | The position at which the conversation opens |
MessageScrollerItem
| Prop | Type | Default | Description |
|---|---|---|---|
messageId | string | None | The id that scrollToMessage and the visibility hook refer to |
scrollAnchor | boolean | false | Marks the row as the start of a turn |
MessageScrollerButton
| Prop | Type | Default | Description |
|---|---|---|---|
direction | 'start' | 'end' | 'end' | The edge that the button is positioned at and scrolls to |
behavior | ScrollBehavior | 'smooth' | How the viewport scrolls when users click the button |
variant | A Button variant | 'secondary' | Passed to the Button. At the default, the Message Scroller’s own classes replace its fill, its border, and its text color. |
size | A Button size | 'icon-sm' | The size of the button |
Children replace the arrow and its label.