MessageScroller basic usage example
Basic usage wraps messages in a MessageScrollerProvider with MessageScrollerViewport containing MessageScrollerContent. Each message is wrapped in MessageScrollerItem with a unique messageId and optional scrollAnchor prop. MessageScrollerButton is placed for scroll controls. The MessageScroller fills its parent, so place it inside a height-constrained container.
MessageScroller data-autoscrolling attribute
The root and viewport expose data-autoscrolling while the programmatic scroll to the latest message runs when autoScroll is enabled. This allows conditionally applying styles during the scroll transition.
MessageScrollerItem messageId requirement
Use stable messageId values for message rows on MessageScrollerItem. This gives the scroller a specific row to preserve when history loads and is necessary for scrollToMessage and useMessageScrollerVisibility hooks to work properly. Rows that need to be addressable should have stable ids.
MessageScroller container height requirement
MessageScroller fills its parent, so place it inside a height-constrained container. Example: wrap in a div with class 'flex h-screen flex-col' and apply flex-1 to MessageScroller to make it fill available space.
Group chat turn boundaries
In group chat, the turn boundary is more specific than 'the user message'. It is often the message that asks the model to respond or a marker like 'Marcus joined the chat'. Typing indicators and history controls usually should not anchor. Because anchoring is role-independent, you can anchor a marker just as easily as a message.
MessageScroller virtualization example with @tanstack/react-virtual
Example virtualization code using @tanstack/react-virtual with MessageScroller: const MotionMessageScrollerItem = motion.create(MessageScrollerItem); Use useVirtualizer with count, getScrollElement, estimateSize, getItemKey, and overscan options. Wrap virtualized items in MessageScrollerViewport and MessageScrollerContent with a container div. Each virtual item renders as a div with ref={virtualizer.measureElement}, data-index, absolute positioning, and transform: translateY for layout.
scrollToMessage queuing behavior
scrollToMessage can queue a target before items exist, which covers client-resolved permalinks while the transcript mounts. After rows have mounted, a missing id returns false instead of starting a guessed retry loop. A true result means the scroll ran or was queued, not that the row is already in view.
MessageScroller imports
Import MessageScroller and related components from @/components/ui/message-scroller: Message, MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport.
MessageScroller basic usage example
A basic MessageScroller usage wraps the component structure as follows: <MessageScrollerProvider> contains <MessageScroller> which contains <MessageScrollerViewport>, which contains <MessageScrollerContent> with mapped <MessageScrollerItem> components, each wrapping a <Message />. Below the content is <MessageScrollerButton />. The MessageScroller fills its parent, so place it inside a height-constrained container like a flex column with h-screen.
MessageScroller anchoring with scrollAnchor prop
A turn is the part of the conversation that starts a new exchange. An anchor is the row the viewport should treat as the start of that turn. Mark a row with scrollAnchor={true} on MessageScrollerItem to tell the scroller to anchor that row. When a new anchor is appended, the viewport moves it near the top and keeps a peek of the previous item above it, so the new turn does not feel detached from context. Scroll anchors are not tied to message role and can anchor any row including user messages, system markers, handoff events, or other meaningful turn starts.
useMessageScroller hook for scroll commands
Import and use the useMessageScroller hook from @/components/ui/message-scroller to drive the transcript from outside the message list. It provides scrollToMessage, scrollToEnd, and scrollToStart functions. scrollToMessage targets the messageId on MessageScrollerItem and returns false when the target is not mounted and cannot be queued. scrollToMessage can queue a target before items exist for client-resolved permalinks while the transcript mounts. After rows have mounted, a missing id returns false.
useMessageScrollerVisibility hook
Import and use the useMessageScrollerVisibility hook from @/components/ui/message-scroller to track the reader's position in the conversation. It provides currentAnchorId and visibleMessageIds. currentAnchorId reports the current anchored turn and stays set after that anchor scrolls above the viewport, answering 'where am I'. visibleMessageIds reports what is on screen in document order, answering 'what is on screen'. Visibility tracking is pay-for-what-you-use and only runs while something subscribes to this hook; rows need a messageId to participate.
useMessageScrollerScrollable hook
Import and use the useMessageScrollerScrollable hook from @/components/ui/message-scroller when you need scroll state in JavaScript such as a status indicator or custom 'jump to latest' control. It provides start and end boolean values reporting which edges the viewport can still scroll toward. 'At the start/end' is the negation (!start / !end), and 'scrollable at all' is start || end. For styling the scroller itself, prefer the data-scrollable attribute.
MessageScroller virtualization example
Example of virtualizing MessageScroller with @tanstack/react-virtual: Import useVirtualizer from @tanstack/react-virtual. Create a VirtualizedTranscript component with viewportRef, initialize useVirtualizer with count (messages.length), getScrollElement returning viewportRef.current, estimateSize (e.g., 86 for typical row height), getItemKey returning stable message id, and overscan (e.g., 8). Wrap in MessageScrollerProvider and MessageScroller. MessageScrollerViewport receives the viewportRef. MessageScrollerContent className="block min-h-full" contains a relative div with height set to virtualizer.getTotalSize(). Map virtualizer.getVirtualItems() to positioned divs with ref={virtualizer.measureElement}, data-index, absolute positioning with transform: translateY(${virtualItem.start}px), and width: full containing your Message component.
MessageScrollerItem animation with motion
MessageScrollerItem can be animated directly. Create a motion version with motion.create(MessageScrollerItem), keep messageId and scrollAnchor on it, and use transform and opacity for the entrance. A common pattern is to animate the user's message when sent by starting it below its final position so it feels like it rises from the live edge of the viewport, then let the assistant reply stream into a regular row below it. Avoid animating height, margin, or padding for row entrances as those changes can fight the scroller's positioning work. If the reader prefers reduced motion, skip the entrance animation and keep the scroll behavior the same.