StorybookGitHub

Message Scroller

An auto-scrolling viewport for chat-style message lists that tracks the live edge and lets users scroll away and back.

The scroll behavior in our chat feels jarring whenever the AI streams a reply.
Wrap your message list in MessageScroller and turn on autoScroll — the viewport pins to the bottom as tokens arrive.
What about when someone sends a brand new message?
MessageScrollerItem fixes that with turn anchoring — set scrollAnchor on the turn that should settle near the top.
And if I've scrolled up to re-read an older answer?
Auto-scroll only runs when the viewport is already pinned to the bottom, otherwise the jump-to-end button appears.

Installation

npx shadcn-vue@latest add https://vuedocs.canceydejean.dev/r/message-scroller.json

Usage

Use message scroller to wrap a chat or feed-style list that should auto-follow new content while the reader is at the bottom, but stay put the moment they scroll away. Wrap the list in MessageScrollerProvider, render items inside MessageScrollerViewport > MessageScrollerContent, and mark each row with MessageScrollerItem so the engine can measure and anchor around it.

<script setup lang="ts">
import {
  MessageScroller,
  MessageScrollerButton,
  MessageScrollerContent,
  MessageScrollerItem,
  MessageScrollerProvider,
  MessageScrollerViewport,
} from "@/components/ui/message-scroller";

const messages = [
  { id: "1", text: "Hey, are you around?" },
  { id: "2", text: "Just pushed the new build." },
];
</script>

<template>
  <MessageScrollerProvider auto-scroll default-scroll-position="end">
    <MessageScroller class="h-96 rounded-lg border">
      <MessageScrollerViewport>
        <MessageScrollerContent>
          <MessageScrollerItem v-for="message in messages" :key="message.id" :message-id="message.id">
            {{ message.text }}
          </MessageScrollerItem>
        </MessageScrollerContent>
      </MessageScrollerViewport>
      <MessageScrollerButton />
    </MessageScroller>
  </MessageScrollerProvider>
</template>

MessageScrollerButton reads scroll state from context and fades itself out when the corresponding edge isn't scrollable, so it can be dropped in without extra wiring. The useMessageScroller composable exposes scrollToEnd, scrollToStart, and scrollToMessage for imperative control from outside the template.