Message Scroller
The message scroller is the headless layer of the package. It follows streaming output only while the reader remains at the live edge, preserves position when older messages prepend, and can jump to a stable message ID.
<script setup lang="ts">
import {
MessageScrollerButton,
MessageScrollerContent,
MessageScrollerItem,
MessageScrollerRoot,
MessageScrollerViewport,
} from "@vite-hub/ui/headless";
const messages = [
{ id: "message-1", text: "Inspect the current implementation." },
{ id: "message-2", text: "The virtual rows use a fixed height." },
{ id: "message-3", text: "Use cumulative offsets for described rows." },
{ id: "message-4", text: "The regression test now passes." },
];
</script>
<template>
<MessageScrollerRoot class="relative h-72 overflow-hidden rounded-md border border-default bg-default">
<MessageScrollerViewport class="h-full overflow-y-auto">
<MessageScrollerContent :items="messages.map(message => message.id)" class="space-y-3 p-4">
<MessageScrollerItem
v-for="message in messages"
:key="message.id"
:message-id="message.id"
class="rounded-md border border-default bg-elevated p-3 text-sm"
>
{{ message.text }}
</MessageScrollerItem>
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton class="absolute bottom-3 left-1/2 -translate-x-1/2 rounded-full border border-default bg-default px-3 py-1 text-xs">
Latest
</MessageScrollerButton>
</MessageScrollerRoot>
</template>
Anatomy
<MessageScrollerRoot>
<MessageScrollerViewport>
<MessageScrollerContent :items="messages.map(message => message.id)">
<MessageScrollerItem
v-for="message in messages"
:key="message.id"
:message-id="message.id"
>
{{ message }}
</MessageScrollerItem>
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScrollerRoot>
Import primitives from @vite-hub/ui/headless when you do not want the styled chat component.
Root props
| Prop | Type | Default |
|---|---|---|
autoScroll | boolean | true |
defaultScrollPosition | 'start' | 'end' | 'end' |
edgeThreshold | number | 8 |
previousItemPeek | number | 64 |
Composable
useMessageScroller() exposes reactive atEnd and isScrollable values plus scrollToEnd() and scrollToMessage(id). Call it under MessageScrollerRoot.
Accessibility
The viewport is a labelled, keyboard-scrollable region and the content is an additions-only log. AgentChat marks that log busy while a response is submitted or streaming, which prevents partial updates from being announced as settled content.
MessageScrollerButton has a default accessible label and uses a native button by default. It stays mounted but inert at the live edge, moves focus back to the viewport when activated, and replaces smooth scrolling with instant scrolling when the reader requests reduced motion.