Message scroller
AIThe scrolling viewport for a conversation: it follows new tokens, anchors each new question to the top and shows a jump to latest button.
npx prodular-ui add message-scrollerConversation
Each new question scrolls to the top so the answer has room to stream below it.
"use client"
import * as React from "react"
import { AiMark } from "@/components/ui/ai-mark"import { Bubble, BubbleContent } from "@/components/ui/bubble"import { Button } from "@/components/ui/button"import { Message, MessageContent, MessageHeader } from "@/components/ui/message"import { MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport,} from "@/components/ui/message-scroller"
const turns = [ ["Which vendors went over budget in Q3?", "Four vendors went over: Northbeam Logistics by $38,200, Kestrel Analytics by $6,450, Vantage Print by $2,100 and Orbis Cloud by $1,240. Northbeam accounts for 81% of the overrun."], ["Why is Northbeam so far over?", "Freight volume to Rotterdam doubled in August after the Halcyon launch, and the contract has no volume discount above 400 pallets a month."], ["Is there a better rate anywhere?", "The 2025 quote from Meridian Freight was 14% lower above 400 pallets. It expired in March, so we would need a new quote."], ["Draft an email asking Meridian for a new quote.", "Here is a draft to Leah Brooks at Meridian, asking for a 12 month rate on 400 to 900 pallets a month to Rotterdam, starting in December."],]
export default function MessageScrollerThread() { const [count, setCount] = React.useState(3) return ( <div className="flex w-full flex-col gap-3"> <div className="h-96 w-full overflow-hidden rounded-xl border bg-background"> <MessageScrollerProvider autoScroll> <MessageScroller> <MessageScrollerViewport> <MessageScrollerContent className="mx-auto max-w-xl px-4 py-6"> {turns.slice(0, count).map(([question, answer], i) => ( <React.Fragment key={i}> <MessageScrollerItem messageId={`q${i}`} scrollAnchor> <Message from="user"> <MessageContent> <Bubble align="end"> <BubbleContent>{question}</BubbleContent> </Bubble> </MessageContent> </Message> </MessageScrollerItem> <MessageScrollerItem messageId={`a${i}`}> <Message from="assistant"> <MessageContent className="gap-1"> <MessageHeader> <AiMark /> Ledgerline Copilot </MessageHeader> <p>{answer}</p> </MessageContent> </Message> </MessageScrollerItem> </React.Fragment> ))} </MessageScrollerContent> </MessageScrollerViewport> <MessageScrollerButton /> </MessageScroller> </MessageScrollerProvider> </div> <Button variant="outline" size="sm" className="self-start" onClick={() => setCount((c) => (c >= turns.length ? 1 : c + 1))} > {count >= turns.length ? "Start over" : "Ask the next question"} </Button> </div> )}Starting at the top
Activity logs read from the start. Buttons jump to either end.
"use client"
import { Message, MessageContent, MessageHeader, MessageMeta } from "@/components/ui/message"import { MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport,} from "@/components/ui/message-scroller"
const events = [ "Pulled 1,284 card transactions from the September statement", "Matched 1,251 transactions to receipts", "Flagged 19 transactions without receipts", "Asked 7 cardholders for missing receipts", "Coded 1,262 transactions to GL accounts", "Sent 22 transactions to Priya Shah for review", "Posted the September card journal entry", "Closed the September card reconciliation",]
export default function MessageScrollerHistory() { return ( <div className="h-72 w-full overflow-hidden rounded-xl border bg-background"> <MessageScrollerProvider defaultScrollPosition="start"> <MessageScroller> <MessageScrollerViewport aria-label="Agent activity"> <MessageScrollerContent className="mx-auto max-w-xl gap-4 px-4 py-5"> {events.map((event, i) => ( <MessageScrollerItem key={event} messageId={`e${i}`}> <Message from="assistant"> <MessageContent className="gap-0"> <MessageHeader className="h-auto"> Step {i + 1} <MessageMeta>{`9:${10 + i * 4} AM`}</MessageMeta> </MessageHeader> <p className="text-sm text-muted-foreground">{event}</p> </MessageContent> </Message> </MessageScrollerItem> ))} </MessageScrollerContent> </MessageScrollerViewport> <MessageScrollerButton direction="start" /> <MessageScrollerButton /> </MessageScroller> </MessageScrollerProvider> </div> )}Anatomy
<MessageScrollerProvider autoScroll> <MessageScroller> <MessageScrollerViewport> <MessageScrollerContent> <MessageScrollerItem messageId="m1" scrollAnchor>...</MessageScrollerItem> </MessageScrollerContent> </MessageScrollerViewport> <MessageScrollerButton /> </MessageScroller></MessageScrollerProvider>Guidance
Use it when
- Around every chat thread or agent log that can grow past the screen.
- Inside a parent with a fixed height, usually the space between the header and the composer.
Avoid
- Scrolling the whole page for a chat. The composer should stay in view.
- Calling scrollIntoView yourself while streaming. The scroller already follows the end.
Rules for AI agents
Published in llms-full.txt, the registry and the MCP server, so coding agents follow them when they build with this component.
- Wrap with MessageScrollerProvider autoScroll so streamed tokens keep the latest line in view unless the user scrolls up.
- Give every MessageScrollerItem a stable messageId. Set scrollAnchor on user messages so each new question moves to the top.
- Give the MessageScroller parent a fixed or flex constrained height and min-h-0, or it will not scroll.
- Render MessageScrollerButton once inside MessageScroller. It hides itself when the view is already at the end.
API
MessageScrollerProvider
| Prop | Type | Default | Description |
|---|---|---|---|
| autoScroll | boolean | false | Follow new content while the view is at the end. |
| defaultScrollPosition | "end" | "start" | "last-anchor" | "end" | Where the view starts. |
MessageScrollerItem
| Prop | Type | Default | Description |
|---|---|---|---|
| messageId | string | Stable id used for anchoring and scrollToMessage. | |
| scrollAnchor | boolean | false | Scroll this item to the top when it is added. |
MessageScrollerButton
| Prop | Type | Default | Description |
|---|---|---|---|
| direction | "end" | "start" | "end" | Which end the button jumps to. |
Installation
The Prodular UI CLI adds the source to components/ui with everything it depends on. In a project that already uses shadcn/ui, the shadcn CLI installs it from the Prodular registry too. See Installation for the first setup.
npx prodular-ui add message-scroller# or, with the registry in components.jsonnpx shadcn@latest add @prodular/message-scroller