Message Scroller
A chat viewport with stick-to-bottom autoscroll, anchoring and visibility tracking.
Overview
The conversation viewport: it sticks to the live edge while new messages stream
in, lets go the moment the reader scrolls away, and offers a tonal IconButton
to jump back to the latest message. Turns can be anchored so a long answer
unrolls beneath the question that produced it, older history can be prepended
without moving the reader, and hooks expose the scroll API, the visible turns
and the scrollable state.
The frame reads on the kit's conversation voice — type-body in on-surface,
the same scale as Message and Bubble — and the scrollbar is tinted with the
outline-variant hairline so it recedes into the surface. Items are
content-visibility-contained, so long histories stay cheap to render. Built on
the @shadcn/react message-scroller primitive.
$ npx morpha-ui add message-scrollerExamples
New chat
An empty thread, a composer, and a streamed answer. autoScroll keeps the live
edge in view while the response is written; the empty state sits in the same
viewport, so nothing shifts when the first turn lands.
Anchoring turns
scrollAnchor on the user's turn parks it near the top of the viewport instead
of the bottom, so the assistant's answer unrolls underneath the question rather
than pushing it off screen.
Each question is a `scrollAnchor` — it parks at the top of the frame.
Group chat
Several participants, with Marker day separators doubling as the scroll
anchors — jumping through the thread lands on a day, not mid-sentence.
Keeping context visible
scrollPreviousItemPeek leaves a sliver of the previous turn on screen when the
next one is anchored, so the thread never looks like it jumped to a blank page.
Toggle it off to see the anchored question land flush against the top edge.
Following the live edge
While a response streams, the viewport follows it. Scroll up mid-stream and autoscroll releases immediately — the reader always wins — and the jump-to-latest button slides in from the bottom edge.
Scroll up mid-stream: autoscroll releases and the button appears.
Opening saved threads
defaultScrollPosition decides where a reopened conversation lands: at the top
(start), on the newest message (end), or on the last question you asked
(last-anchor).
Opens on the last question you asked.
Load history
With preserveScrollOnPrepend (the default), prepending an older page of
messages keeps the reader exactly where they were — the content grows above the
viewport instead of under it.
Animating new messages
New turns arrive on the kit's motion: a few pixels of rise plus a soft blur that
clears, on one strong ease-out curve. Nothing appears from nothing, and
prefers-reduced-motion keeps the fade while dropping the travel.
Commands
useMessageScroller() exposes scrollToStart, scrollToEnd and
scrollToMessage(id, options) — drive the viewport from any control inside the
provider.
Transcript outline
useMessageScrollerVisibility() reports the currentAnchorId and the visible
message ids: enough to keep a side outline in sync with the transcript, and to
scroll back to a section when it is clicked.
Scroll status
useMessageScrollerScrollable() tells you which directions still have content —
useful for an "unread below" line, or to confirm the whole thread is in view.
Installation
Morpha UI components are distributed via the Morpha CLI: npx morpha-ui add message-scroller. In the meantime, since this is the base shadcn component,
you can also add it directly:
npx shadcn@latest add message-scrollerAPI Reference
Anatomy
<MessageScrollerProvider autoScroll>
<MessageScroller>
<MessageScrollerViewport>
<MessageScrollerContent>
<MessageScrollerItem messageId={id} scrollAnchor={role === 'user'}>
<Message />
</MessageScrollerItem>
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>MessageScrollerProvider
Owns scroll state, anchoring, autoscroll and visibility tracking for everything inside it. The hooks only work under a provider.
Prop
Type
MessageScroller
The frame: a relatively positioned column that clips its own overflow and hosts
the scroll buttons. It has no props of its own — a plain div that accepts
className plus any native attribute.
MessageScrollerViewport
The scrolling element.
Prop
Type
MessageScrollerContent
The transcript container — a live region with a trailing spacer that lets the last anchored turn reach the top of the viewport.
Prop
Type
MessageScrollerItem
One row of the transcript.
Prop
Type
MessageScrollerButton
The jump-to-edge affordance, rendered as the kit's tonal IconButton.
Prop
Type
Hooks
useMessageScroller()—scrollToStart(options),scrollToEnd(options),scrollToMessage(id, options). Options:align,behavior,scrollMargin.useMessageScrollerVisibility()—{ currentAnchorId, visibleMessageIds }.useMessageScrollerScrollable()—{ start, end }: whether content remains in each direction.
Accessibility
- The viewport is a normal scroll container — keyboard scrolling and screen-reader reading order are preserved, and it takes the kit's secondary focus ring when tabbed to.
- The content is a polite live region: streamed additions are announced without interrupting the reader.
- The scroll-to-edge button has a visually hidden label and leaves the tab order when inactive.
- Autoscroll stops the moment the user scrolls — never fight the reader's position.
- Give every row a stable
messageIdwhen you drive the viewport from an outline or from commands: that id is what those controls scroll to.