MessageScroller

A message list that sticks to the newest message, with a scroll-to-bottom button and lazy-rendered items.

How does the message scroller behave?
Autoscroll only follows while you're at the bottom.
const replies = /* ... */;

<div className="flex h-72 w-full max-w-md flex-col gap-3">
  <MessageScrollerProvider autoScroll>
    <MessageScroller>
      <MessageScrollerViewport>
        <MessageScrollerContent className="p-4">
          {messages.map((message) => (
            <MessageScrollerItem key={message.id}>
              <Message align={message.role === "user" ? "end" : "start"}>
                <MessageContent>
                  {message.role === "user" ? (
                    <Bubble align="end" variant="muted">
                      <BubbleContent>{message.text}</BubbleContent>
                    </Bubble>
                  ) : (
                    message.text
                  )}
                </MessageContent>
              </Message>
            </MessageScrollerItem>
          ))}
        </MessageScrollerContent>
      </MessageScrollerViewport>
      <MessageScrollerButton />
    </MessageScroller>
  </MessageScrollerProvider>
  <Button onPress={addMessage} className="self-center">
    Add message
  </Button>
</div>

Installation

npx shadcn@latest add @dotui/message-scroller

Usage

import {
  MessageScroller,
  MessageScrollerButton,
  MessageScrollerContent,
  MessageScrollerItem,
  MessageScrollerProvider,
  MessageScrollerViewport,
} from "@/ui/message-scroller"
<MessageScrollerProvider>
  <MessageScroller>
    <MessageScrollerViewport>
      <MessageScrollerContent>
        {messages.map((message) => (
          <MessageScrollerItem key={message.id}>
            {/* message */}
          </MessageScrollerItem>
        ))}
      </MessageScrollerContent>
    </MessageScrollerViewport>
    <MessageScrollerButton />
  </MessageScroller>
</MessageScrollerProvider>

Anatomy

MessageScrollerProvider holds the scroll state — autoscroll follows new content while the reader sits at the bottom edge and lets go the moment they scroll away. MessageScrollerViewport is the scrollable area, MessageScrollerContent lays the messages out, and each MessageScrollerItem renders lazily as it nears the viewport, so long conversations stay cheap. MessageScrollerButton appears once the reader leaves the edge and scrolls them back to the newest message.

The useMessageScroller, useMessageScrollerScrollable and useMessageScrollerVisibility hooks expose imperative scrolling and scroll state to anything inside the provider.

Examples

Basic

How do I center a div?
Give the parent a grid display and center its place-items. Two lines, no margins to guess at.
And if the parent is already a flex row?
Then centering justify-content and align-items does the same job along the two axes.
What about absolutely positioned children?
Inset all four sides to zero and give the child auto margins — it centers in both dimensions.

API Reference

MessageScrollerProvider

Holds the scroller's state — autoscroll behavior, the default scroll position, and edge thresholds. Wrap it around the scroller and anything that calls its hooks.

MessageScroller

A message list that sticks to the newest message — it follows new content while the reader is at the edge and stays put once they scroll away.

PropType

MessageScrollerViewport

The scrollable area of the scroller.

PropType

MessageScrollerContent

Lays out the messages inside the viewport.

PropType

MessageScrollerItem

Wraps one message. Rendered lazily as it nears the viewport, and usable as a scroll anchor.

PropType

MessageScrollerButton

The scroll-to-edge button. Appears once the reader scrolls away from the edge it points at, and hides again at the edge.

PropType

Last updated on 8/31/2026