Message Scroller Auto-scrolling chat viewport built on the shadcn MessageScroller primitive.
TSX
"use client" ;
import * as React from "react" ;
import { Bubble, BubbleContent } from "@/components/ui/bubble" ;
import { Button } from "@/components/ui/button" ;
import {
MessageScroller,
MessageScrollerButton,
MessageScrollerContent,
MessageScrollerItem,
MessageScrollerProvider,
MessageScrollerViewport,
} from "@/components/ui/message-scroller" ;
type ChatMessage = { id : string ; from : "emma" | "you" ; text : string };
const seed : ChatMessage [] = [
{ id: "m1" , from: "emma" , text: "Morning! Ready for the release?" },
{ id: "m2" , from: "you" , text: "Almost — docs are the last piece." },
{ id: "m3" , from: "emma" , text: "Which components are left?" },
{ id: "m4" , from: "you" , text: "The chat family: bubble, message, scroller." },
{ id: "m5" , from: "emma" , text: "Nice. Screenshots for the changelog too?" },
{ id: "m6" , from: "you" , text: "Good call, I'll capture them today." },
{ id: "m7" , from: "emma" , text: "Then let's ship tomorrow morning." },
{ id: "m8" , from: "you" , text: "Works for me. Scroll up to test the button!" },
];
const replies = [
"Auto-scroll keeps you pinned to the newest message." ,
"Unless you've scrolled up — then it won't interrupt you." ,
"The arrow button brings you back to the live edge." ,
];
export default function MessageScrollerDemo () {
const [ messages , setMessages ] = React. useState (seed);
function simulateReply () {
setMessages (( prev ) => [
... prev,
{
id: `m${ prev . length + 1 }` ,
from: "emma" ,
text: replies[(prev. length - seed. length ) % replies. length ],
},
]);
}
return (
< div className = "flex h-96 w-full max-w-md flex-col gap-3" >
< MessageScrollerProvider autoScroll >
< MessageScroller className = "flex-1 rounded-xl border" >
< MessageScrollerViewport className = "p-3" aria-label = "Conversation" >
< MessageScrollerContent className = "gap-2" >
{messages. map (( message ) => (
< MessageScrollerItem
key = {message.id}
messageId = {message.id}
scrollAnchor = {message.from === "you" }
className = "flex flex-col"
>
< Bubble
variant = {message.from === "emma" ? "muted" : "default" }
align = {message.from === "you" ? "end" : "start" }
>
< BubbleContent >{message.text}</ BubbleContent >
</ Bubble >
</ MessageScrollerItem >
))}
</ MessageScrollerContent >
</ MessageScrollerViewport >
< MessageScrollerButton />
</ MessageScroller >
</ MessageScrollerProvider >
< Button variant = "outline" onClick = {simulateReply}>
Simulate reply
</ Button >
</ div >
);
}
Bash
Package manager: npm pnpm yarn bun
Depends on the @shadcn/react package, installed automatically.
TSX
A chat scroll container that follows streaming output while the reader
sits at the live edge, disengages the moment they scroll away, and
preserves the visible rows when older history loads above. Rows are real
DOM (no virtualization); content-visibility: auto keeps hundreds of
turns cheap. Give items stable messageIds for reliable anchoring and
jump-to.
Headless root owning scroll state and commands.
Prop Type Default autoScrollbooleantruedefaultScrollPosition"start" | "end" | "last-anchor""end"scrollEdgeThresholdnumber— scrollPreviousItemPeeknumber— scrollMarginnumber—
Use defaultScrollPosition="last-anchor" when reopening saved threads;
scrollPreviousItemPeek keeps a slice of the previous turn visible above
a new anchor.
The styled frame (positions the scroll button) and the transcript
column. Extend div via the primitive.
The scrollable element. Label it with aria-label.
Prop Type Default preserveScrollOnPrependbooleantrue
Wraps each row for measurement and visibility tracking.
Prop Type Default messageIdstring— scrollAnchorbooleanfalse
Mark conversation-turn boundaries (typically user messages) with
scrollAnchor.
Scroll control rendered through Button
(secondary / icon-sm by default, swap via render, variant,
size). Hidden and inert while there is nothing to scroll toward.
Prop Type Default direction"start" | "end""end"
useMessageScroller() — returns scrollToMessage(messageId),
scrollToEnd(), scrollToStart() for external controls.
useMessageScrollerVisibility() — currentAnchorId and
visibleMessageIds for position tracking (costs nothing until used).
useMessageScrollerScrollable() — whether the viewport can scroll
toward start / end.
Was this page helpful?
Thanks for your feedback!