Components / Message Scroller

Message Scroller

The scrollable transcript region for chat UIs: anchored turns that keep part of the previous message in view, an AutoScroll follow that disengages the moment the reader scrolls away, prepend preservation for loading older history, and jump-to-message. A chat extra outside the Base UI set, no Radix or Base UI counterpart, same precedent as DataGrid. Click "Add message" below and scroll up to feel the disengage. This preview is the real component running in WebAssembly.

Examples

Today
How do I keep the reader's place while a reply streams in?
Anchor the turn near the top and let the answer grow into the screen below it.
Does it follow new messages automatically?
Only while you are already at the live edge. Scroll away and new content arrives offscreen.
razor
<ZitsMessageScrollerProvider AutoScroll="true" DefaultScrollPosition="end">
    <ZitsMessageScroller class="h-80 rounded-2xl border">
        <ZitsMessageScrollerViewport class="px-4 py-3">
            <ZitsMessageScrollerContent>
                @foreach (var m in _messages)
                {
                    <ZitsMessageScrollerItem @key="m.Id" MessageId="m.Id" ScrollAnchor="m.Anchor">
                        <ZitsMessage Align="@(m.Role == "user" ? "end" : "start")">
                            <ZitsMessageContent>
                                <ZitsBubble>
                                    <ZitsBubbleContent>@m.Text</ZitsBubbleContent>
                                </ZitsBubble>
                            </ZitsMessageContent>
                        </ZitsMessage>
                    </ZitsMessageScrollerItem>
                }
            </ZitsMessageScrollerContent>
        </ZitsMessageScrollerViewport>
        <ZitsMessageScrollerButton />
    </ZitsMessageScroller>
</ZitsMessageScrollerProvider>

Anatomy

The provider renders no DOM of its own: it owns the scroll state and cascades it to everything inside, so the frame can sit anywhere below it.

razor
ZitsMessageScrollerProvider          (no DOM; owns scroll state)
└── ZitsMessageScroller              (frame, fills its parent)
    ├── ZitsMessageScrollerViewport  (scroll container)
    │   └── ZitsMessageScrollerContent
    │       └── ZitsMessageScrollerItem   (one per row)
    └── ZitsMessageScrollerButton    (scroll-to-end control)

Installation

Terminal bash
navius add chat

API reference

ZitsMessageScrollerProvider (root, no DOM):

Prop Type Default
AutoScroll bool false
DefaultScrollPosition string "end"
ScrollEdgeThreshold double 8
ScrollMargin double 0
ScrollPreviousItemPeek double 64

ZitsMessageScrollerViewport (the scroll container, needs a bounded height from its ancestors):

Prop Type Default
PreserveScrollOnPrepend bool true

ZitsMessageScrollerContent:

Prop Type Default
SpacerClass string? -

ZitsMessageScrollerItem (one per row; give it a stable MessageId and @key):

Prop Type Default
MessageId string? -
ScrollAnchor bool false

ZitsMessageScrollerButton (scroll-to-start/end control, appears only while the viewport can scroll that way):

Prop Type Default
Direction string "end"
Behavior string "smooth"
Variant string "secondary"
Size string "icon-sm"

For programmatic control, inject the cascaded MessageScrollerContext (from Navius.Primitives.Components.MessageScroller) anywhere inside the provider: ScrollToMessageAsync, ScrollToStartAsync, ScrollToEndAsync, plus the ScrollableStart/ScrollableEnd, CurrentAnchorId, and VisibleMessageIds readouts.