Message Scroller

Chat transcript scroller for turn anchoring, streaming follow, history restore, and jump controls.

MessageScroller owns the transcript viewport: turn anchoring, live-edge follow, opening position, prepend restore, jump commands, and visibility. It does not own messages, transport, or the prompt composer - pair it with Message and Bubble.

How can I help you today?
ER
I'm building a chat and the scroll jumps while the model streams.
Anchor each user turn, enable autoScroll for the live edge, and keep a peek of the previous item.
ER
What about reopening a saved transcript?
Open on last-anchor so the reader lands on the last meaningful turn, not the absolute bottom.

Installation

pnpm dlx cubix@latest add message-scroller

Usage

Import
import {
  MessageScroller,
  MessageScrollerButton,
  MessageScrollerContent,
  MessageScrollerItem,
  MessageScrollerProvider,
  MessageScrollerViewport,
} from "@/components/cubix/message-scroller"
Example
<MessageScrollerProvider autoScroll>
  <MessageScroller>
    <MessageScrollerViewport>
      <MessageScrollerContent>
        {messages.map((message) => (
          <MessageScrollerItem
            key={message.id}
            messageId={message.id}
            scrollAnchor={message.role === "user"}
          >
            <Message>...</Message>
          </MessageScrollerItem>
        ))}
      </MessageScrollerContent>
    </MessageScrollerViewport>
    <MessageScrollerButton />
  </MessageScroller>
</MessageScrollerProvider>

Note: MessageScroller fills its parent. Place it in a height-constrained container. Wrap every direct content child in MessageScrollerItem.

Composition

 
MessageScrollerProvider
└── MessageScroller
    ├── MessageScrollerViewport
       └── MessageScrollerContent
           └── MessageScrollerItem…
    ├── MessageScrollerButton (start)
    └── MessageScrollerButton (end)

Features

  • Anchors new turns near the top with previous-item peek
  • Follows streamed output only while the reader is at the live edge
  • Opens saved threads on start, end, or last-anchor
  • Preserves place when earlier history is prepended
  • Jumps to any messageId from outside the list
  • Tracks current anchor and visible rows on demand
  • Start and end scroll buttons that stay inert until useful

Anchoring turns

Mark the row that starts a turn with scrollAnchor. Send a turn and toggle which role settles near the top.

Send a message to see the selected role settle near the top.

Following the live edge

With autoScroll, tokens stay in view while the reader remains at the bottom. Scroll away and the position is preserved.

ER
Show me streaming follow-output without fighting my scroll.

Opening saved threads

Use defaultScrollPosition="last-anchor" so a saved transcript opens on the last meaningful turn.

ER
This is the first message the user sent in the conversation.
Workspace creation rose 8%, but first invite completion only rose 2%.
ER
This is the last message the user sent in the conversation.
Start with the invite step. Teams create workspaces but wait to add collaborators.
Recommended follow-up: compare invite drop-off by account size, check 24-hour return, and segment by template.

Loading earlier messages

Prepend older rows without jumping the reader. Stable messageId values keep the visible row locked.

How can I help you today?
ER
I'm building a chat and the scroll jumps while the model streams.
Anchor each user turn, enable autoScroll for the live edge, and keep a peek of the previous item.
ER
What about reopening a saved transcript?
Open on last-anchor so the reader lands on the last meaningful turn, not the absolute bottom.

Jumping to messages

Drive the transcript from outside with useMessageScroller.

ER
We're seeing activation dip after workspace creation.
The sharpest drop is between creating the workspace and inviting the first teammate.
ER
What should I compare before we change onboarding?
Compare template users, blank-workspace users, and users who skip invites then return within 24 hours.
ER
Can you turn that into an experiment?
Yes. Show a short checklist after workspace creation, then measure first invite completion.

Tracking position

useMessageScrollerVisibility reports the current anchor and visible ids. Tracking only runs while something subscribes.

ER
We're seeing activation dip after workspace creation.
The sharpest drop is between creating the workspace and inviting the first teammate.
ER
What should I compare before we change onboarding?
Compare template users, blank-workspace users, and users who skip invites then return within 24 hours.
ER
Can you turn that into an experiment?
Yes. Show a short checklist after workspace creation, then measure first invite completion.

Current anchor: none · Visible: 0

Scrollable edges

useMessageScrollerScrollable reports whether the viewport can still move toward start or end.

How can I help you today?
ER
I'm building a chat and the scroll jumps while the model streams.
Anchor each user turn, enable autoScroll for the live edge, and keep a peek of the previous item.
ER
What about reopening a saved transcript?
Open on last-anchor so the reader lands on the last meaningful turn, not the absolute bottom.

The transcript fits in the viewport.

Group chat anchors

Anchors are role-independent. Mark a join marker as the turn boundary when a participant enters.

ER
@mary, the export line keeps matching Venus energy output.
I can check the math. Ping me again if Rocky joins.

API Reference

MessageScrollerProvider

PropTypeDefaultDescription
autoScrollbooleanfalseFollow streamed output while the reader stays at the live edge.
defaultScrollPosition"start" | "end" | "last-anchor""start"Where a mounted transcript opens.
scrollPreviousItemPeeknumber-Pixels of the previous item kept visible above a newly anchored turn.
scrollEdgeThresholdnumber-Distance from an edge that still counts as being at that edge.
scrollMarginnumber-Default scroll margin used by programmatic scroll commands.

MessageScroller

PropTypeDefaultDescription
classNamestring-Additional classes for the scroller frame.

MessageScrollerViewport

PropTypeDefaultDescription
preserveScrollOnPrependbooleantrueKeep the visible row stable when earlier messages are prepended.
classNamestring-Additional classes for the scrollable viewport.

MessageScrollerContent

PropTypeDefaultDescription
classNamestring-Additional classes for the transcript container.
spacerClassNamestring-Optional classes for the content spacer used by anchoring.

MessageScrollerItem

PropTypeDefaultDescription
messageIdstring-Stable id for jump targets, visibility tracking, and prepend restore.
scrollAnchorbooleanfalseMark this row as the start of a turn for anchoring.
classNamestring-Additional classes for the transcript row.

MessageScrollerButton

PropTypeDefaultDescription
direction"start" | "end""end"Which edge the button scrolls toward.
behaviorScrollBehavior-Native scroll behavior for the button action.
variantButton variant"secondary"Passed through to the Cubix Button render target.
sizeButton size"icon-sm"Passed through to the Cubix Button render target.