Message Scroller
The scroll container for a conversation. It handles the parts that are easy to get wrong: pinning to the bottom while a reply streams, anchoring each new turn near the top, preserving position when history is prepended, jump-to-message, scroll controls, and visibility tracking.
Streaming chat
A read-only chat. Press send to stream a scripted reply: the viewport pins to the bottom while you're caught up, anchors each new turn near the top, and surfaces the scroll button when you scroll away. Assistant replies render markdown via vue-stream-markdown.
New Chat
How can I help you today?
Morning!
What are we working on today? Press send to start a new conversation
Demo is read only. Press send to send messages.
Basic autoScroll
The minimal setup: Provider + Viewport + Content + Items, with autoScroll on. Append a message to watch it follow.
Controls & tracking
jump-to-message, top/bottom controls, prepend-preserve (Load older), and live visibility tracking via the composables.
Default
Provider + Viewport + Content + Items with a scroll-to-latest button. Opens at the newest message; scroll up and the button fades in.
Group chat
A multi-sender thread. Your own turns align to the end; everyone else gets an avatar and a name header on the start.
Streaming reply
With autoScroll on, the viewport pins to the bottom while a reply streams in token by token. Press Replay to watch it again.
Turn anchoring
Mark a turn with scrollAnchor and it settles near the top with a peek of the previous exchange above it, instead of snapping to the bottom.
Opening position
defaultScrollPosition decides where the thread lands on first render: the top, the newest message, or the last anchored turn.
Scrollable edges
A control at each edge. Each button reflects the scrollable state: it appears only when there's more content past that edge.
Preserve context on prepend
preserveScrollOnPrepend keeps the reader's position when older history is added above. Toggle it off to feel the content shift under you.
Load history
Infinite-history pattern: a Load older button prepends a page after a short fetch while your scroll position stays put.
Animated items
New items fade and slide in via a CSS keyframe on a wrapper inside each item, so the entrance never disturbs the engine's measurement.
Imperative commands
useMessageScroller() exposes scrollToStart, scrollToEnd, and scrollToMessage(id) for programmatic jumps from your own controls.
Visibility tracking
useMessageScrollerVisibility() reports which messages are on screen and the current anchor, tracked live via IntersectionObserver.
Anatomy
Import all parts and piece them together.
<script setup>
import {
MessageScroller,
MessageScrollerButton,
MessageScrollerContent,
MessageScrollerItem,
MessageScrollerProvider,
MessageScrollerViewport,
} from "@/components/ui/message-scroller";
</script>
<template>
<MessageScrollerProvider>
<MessageScroller>
<MessageScrollerViewport>
<MessageScrollerContent>
<MessageScrollerItem />
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>
</template>