const replies = /* ... */;
<div className="flex h-72 w-full max-w-md flex-col gap-3">
<MessageScrollerProvider autoScroll>
<MessageScroller>
<MessageScrollerViewport>
<MessageScrollerContent className="p-4">
{messages.map((message) => (
<MessageScrollerItem key={message.id}>
<Message align={message.role === "user" ? "end" : "start"}>
<MessageContent>
{message.role === "user" ? (
<Bubble align="end" variant="muted">
<BubbleContent>{message.text}</BubbleContent>
</Bubble>
) : (
message.text
)}
</MessageContent>
</Message>
</MessageScrollerItem>
))}
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>
<Button onPress={addMessage} className="self-center">
Add message
</Button>
</div>Installation
npx shadcn@latest add @dotui/message-scrollerUsage
import {
MessageScroller,
MessageScrollerButton,
MessageScrollerContent,
MessageScrollerItem,
MessageScrollerProvider,
MessageScrollerViewport,
} from "@/ui/message-scroller"<MessageScrollerProvider>
<MessageScroller>
<MessageScrollerViewport>
<MessageScrollerContent>
{messages.map((message) => (
<MessageScrollerItem key={message.id}>
{/* message */}
</MessageScrollerItem>
))}
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>Anatomy
MessageScrollerProvider holds the scroll state — autoscroll follows new
content while the reader sits at the bottom edge and lets go the moment they
scroll away. MessageScrollerViewport is the scrollable area,
MessageScrollerContent lays the messages out, and each
MessageScrollerItem renders lazily as it nears the viewport, so long
conversations stay cheap. MessageScrollerButton appears once the reader
leaves the edge and scrolls them back to the newest message.
The useMessageScroller, useMessageScrollerScrollable and
useMessageScrollerVisibility hooks expose imperative scrolling and scroll
state to anything inside the provider.
Examples
Basic
API Reference
MessageScrollerProvider
Holds the scroller's state — autoscroll behavior, the default scroll position, and edge thresholds. Wrap it around the scroller and anything that calls its hooks.
MessageScroller
A message list that sticks to the newest message — it follows new content while the reader is at the edge and stays put once they scroll away.
| Prop | Type | Default | |
|---|---|---|---|
MessageScrollerViewport
The scrollable area of the scroller.
| Prop | Type | Default | |
|---|---|---|---|
MessageScrollerContent
Lays out the messages inside the viewport.
| Prop | Type | Default | |
|---|---|---|---|
MessageScrollerItem
Wraps one message. Rendered lazily as it nears the viewport, and usable as a scroll anchor.
| Prop | Type | Default | |
|---|---|---|---|
MessageScrollerButton
The scroll-to-edge button. Appears once the reader scrolls away from the edge it points at, and hides again at the edge.
| Prop | Type | Default | |
|---|---|---|---|
Last updated on 8/31/2026