Skip to content

Shadcn Message Scroller

The shadcn/ui Message Scroller is a chat scroll container that anchors turns, opens saved transcripts, follows streamed responses, loads history without jumping, and jumps to any message, styled with Tailwind CSS v4. Unlike a plain overflow-y-auto div, it only auto-follows the bottom while the reader hasn't scrolled away, so streaming AI replies never fight the user for control of the viewport. Message Scroller is a new shadcn primitive for building AI-chat interfaces, and is the only one of the new chat components backed by its own headless package, @shadcn/react.

Key features

  • Auto-follows the bottom during streaming, but only while the reader is already there — scrolling up is a deliberate opt-out that preserves their position.
  • scrollAnchor on a MessageScrollerItem settles that turn near the top instead of snapping to the document end, with a peek of the prior exchange still visible.
  • MessageScrollerButton appears when there's unseen content below the fold and jumps back to the latest message on click.
  • useMessageScroller, useMessageScrollerScrollable, and useMessageScrollerVisibility hooks for programmatic control and state.
  • MessageScrollerContent defaults to role="log" semantics so assistive tech announces new messages as they stream in.

Installation

bash
npx shadcn@latest add message-scroller

This installs the styled wrapper into @/components/ui and adds @shadcn/react as a dependency — the package that implements the actual scroll-anchoring and auto-follow behavior.

Usage

Wrap the thread in a MessageScrollerProvider, then compose MessageScroller, MessageScrollerViewport, MessageScrollerContent, and one MessageScrollerItem per turn.

tsx
import {
  MessageScroller,
  MessageScrollerButton,
  MessageScrollerContent,
  MessageScrollerItem,
  MessageScrollerProvider,
  MessageScrollerViewport,
} from "@/components/ui/message-scroller"
import { Message, MessageAvatar, MessageContent } from "@/components/ui/message"

export function Example() {
  return (
    <MessageScrollerProvider>
      <MessageScroller className="h-96">
        <MessageScrollerViewport>
          <MessageScrollerContent className="p-4">
            {messages.map((message) => (
              <MessageScrollerItem
                key={message.id}
                messageId={message.id}
                scrollAnchor={message.role === "user"}
              >
                <Message align={message.role === "user" ? "end" : "start"}>
                  <MessageAvatar>{/* … */}</MessageAvatar>
                  <MessageContent>{message.text}</MessageContent>
                </Message>
              </MessageScrollerItem>
            ))}
          </MessageScrollerContent>
        </MessageScrollerViewport>
        <MessageScrollerButton />
      </MessageScroller>
    </MessageScrollerProvider>
  )
}

Turn anchoring

Set scrollAnchor on the MessageScrollerItem that should settle near the top of the viewport when a new turn arrives, instead of snapping straight to the bottom. In the demo above, the current user's own messages are anchored so a reply starts in view without the disorienting jump of a plain overflow container.

Programmatic scrolling

tsx
import { useMessageScroller } from "@/components/ui/message-scroller"

function JumpToLatest() {
  const { scrollToEnd, scrollToMessage } = useMessageScroller()
  return <button onClick={() => scrollToEnd({ behavior: "smooth" })}>Latest</button>
}

Accessibility

  • MessageScrollerContent sets role="log" and aria-relevant="additions" by default, so screen readers announce new messages as they stream in.
  • MessageScrollerButton is a real <button> with an sr-only label, and drops out of the tab order once the viewport is already at the bottom — no ghost focus stop.
  • Keep each MessageScrollerItem keyed by a stable messageId so scroll-to-message and visibility tracking stay accurate as the list updates.

When to use the Message Scroller

Use Message Scroller for any chat or AI-assistant thread where messages stream in over time. For a static, non-streaming list that doesn't need auto-follow or anchoring, a plain scrollable container is enough — reach for Scroll Area instead.

Used in these blocks

Message Scroller ships with ShadcnStore's upcoming AI-Chat blocks (next release), powering the scrollable thread behind streamed assistant replies. This section will link to the real blocks once they're live.

Message Scroller wraps a list of Message turns, each typically paired with an Avatar; use a Marker for status lines between turns. For scrollable content that isn't a chat thread, use Scroll Area instead.

FAQ

What is the shadcn message scroller used for?

Message Scroller is a scroll container for chat threads that anchors turns, follows streamed AI responses while the reader is at the bottom, and shows a scroll-to-latest button when new content arrives off-screen.

How is Message Scroller different from a plain overflow container?

A plain overflow container just clips and scrolls. Message Scroller additionally auto-follows the bottom during streaming, anchors a chosen turn near the top instead of snapping to the document end, and only auto-scrolls when the reader hasn't deliberately scrolled away.

Does Message Scroller require an extra dependency?

Yes. Its scroll-anchoring and auto-follow logic ships in the @shadcn/react package; the CLI installs it automatically alongside the styled wrapper.

Can I jump to a specific message?

Yes. Call scrollToMessage(messageId) from the useMessageScroller hook, matching the messageId prop on the target MessageScrollerItem.