Message Scroller
A scrollable message container with auto-scroll and jump-to-end button.
When to use
MessageScroller is for a live transcript: it holds the newest message in view while output streams in and reveals a jump-to-latest button once the user scrolls away. A bounded static list is a ScrollArea instead, and the turns inside the transcript are still Message and Bubble.
Platform notes
It needs the full nesting (Provider, Root, Viewport, Content, Item) inside a parent with a resolved height, h-80 in the demo, or autoscroll has nothing to measure and the button never activates. Unlike the rest of the catalog it pulls a runtime dependency, @shadcn/react, which is not in the ARCHITECTURE.md stack table, so record the deviation in the app before adopting it. MessageScrollerItem sets content-visibility: auto with an intrinsic size hint, so give images and embeds explicit dimensions or the transcript jumps as items render. The jump button ships a hardcoded English sr-only label that has to become an i18next message, and the viewport asks for a scroll-fade-b utility the theme does not define, so do not rely on that fade to signal more content.
Install
npx shadcn@latest add message-scrollerCode
"use client";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Bubble, BubbleContent } from "@/components/ui/bubble";
import { Message, MessageAvatar, MessageContent, MessageGroup } from "@/components/ui/message";
import {
MessageScroller,
MessageScrollerButton,
MessageScrollerContent,
MessageScrollerItem,
MessageScrollerProvider,
MessageScrollerViewport,
} from "@/components/ui/message-scroller";
import { Bot } from "lucide-react";
const messages = [
{ id: 1, text: "Hey, can you check the new chart component?", sender: "user" },
{ id: 2, text: "Already reviewed it — the Recharts integration looks solid.", sender: "bot" },
{ id: 3, text: "Great. Did the data-table typecheck pass?", sender: "user" },
{ id: 4, text: "Yes, TanStack Table is wired up and working.", sender: "bot" },
{ id: 5, text: "Let's ship it then.", sender: "user" },
];
export function MessageScrollerDemo() {
return (
<div className="h-80 w-full max-w-md rounded-lg border">
<MessageScrollerProvider>
<MessageScroller>
<MessageScrollerViewport>
<MessageScrollerContent>
{messages.map((msg) => (
<MessageScrollerItem key={msg.id}>
<MessageGroup>
<Message align={msg.sender === "user" ? "end" : "start"}>
<MessageAvatar>
<Avatar>
<AvatarFallback>{msg.sender === "user" ? "U" : <Bot />}</AvatarFallback>
</Avatar>
</MessageAvatar>
<MessageContent>
<Bubble
variant={msg.sender === "user" ? "default" : "tinted"}
align={msg.sender === "user" ? "end" : "start"}
>
<BubbleContent>{msg.text}</BubbleContent>
</Bubble>
</MessageContent>
</Message>
</MessageGroup>
</MessageScrollerItem>
))}
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>
</div>
);
}