Message Scroller
A managed scroll region for conversational transcripts: anchored turns, live-edge follow, and prepend-safe history.
<NaviusMessageScrollerProvider AutoScroll="true" DefaultScrollPosition="end" ScrollPreviousItemPeek="64">
<NaviusMessageScroller class="relative flex h-full min-h-0 flex-col overflow-hidden">
<NaviusMessageScrollerViewport class="h-full min-h-0 w-full overflow-y-auto">
<NaviusMessageScrollerContent class="flex h-max min-h-full flex-col gap-3">
@foreach (var m in _messages)
{
<NaviusMessageScrollerItem @key="m.Id" MessageId="@m.Id" ScrollAnchor="m.Anchor">
@* message row *@
</NaviusMessageScrollerItem>
}
</NaviusMessageScrollerContent>
</NaviusMessageScrollerViewport>
<NaviusMessageScrollerButton class="absolute bottom-3 left-1/2 -translate-x-1/2">
<ArrowDownIcon />
</NaviusMessageScrollerButton>
</NaviusMessageScroller>
</NaviusMessageScrollerProvider>Features
- Anchors each newly appended turn (
ScrollAnchor) near the top of the viewport, keeping a peek of the previous item visible above it (ScrollPreviousItemPeek). - Follows the live edge while content streams in (
AutoScroll), but only while the reader is already there; wheel, touch, keyboard scrolling and explicit jumps release it. - Preserves the currently visible row when older messages are prepended (
PreserveScrollOnPrepend), keyed on stableMessageIdvalues. - Opens at the end, the start, or the last anchored turn (
DefaultScrollPosition). - A cascaded
MessageScrollerContextexposes jump commands (ScrollToMessageAsync,ScrollToStartAsync,ScrollToEndAsync), the scrollable-edge state and a lazy visibility subscription, reachable from anywhere inside the Provider, including outside the scroller frame. - The scroll hot path runs entirely in the engine: no rerender of transcript rows on scroll or stream.
Installation
Install the brain, or copy just this primitive in with the CLI.
navius add message-scrollerAnatomy
Import the parts and assemble them.
@using Navius.Primitives.Components.MessageScroller
<NaviusMessageScrollerProvider>
<NaviusMessageScroller>
<NaviusMessageScrollerViewport>
<NaviusMessageScrollerContent>
<NaviusMessageScrollerItem MessageId="m1" ScrollAnchor="true">
@* message *@
</NaviusMessageScrollerItem>
</NaviusMessageScrollerContent>
</NaviusMessageScrollerViewport>
<NaviusMessageScrollerButton />
</NaviusMessageScroller>
</NaviusMessageScrollerProvider>API Reference
Provider
Cascades the scroll options and the shared context to every descendant. Renders no DOM of its own.
| Prop | Type | Default |
|---|---|---|
| AutoScroll | bool | false |
| DefaultScrollPosition | string | "end" |
| ScrollEdgeThreshold | double | 8 |
| ScrollMargin | double | 0 |
| ScrollPreviousItemPeek | double | 64 |
| ChildContent | RenderFragment? | - |
Root
The scroller frame: the positioning parent for the scroll button and the mirror target for the scrollable-edge and autoscrolling state. Must sit in a height-constrained container. Renders a div.
| Prop | Type | Default |
|---|---|---|
| ChildContent | RenderFragment? | - |
| …attributes | IDictionary<string, object>? | - |
Data attributes
| Data attribute | Values |
|---|---|
| [data-navius-messagescroller] | Present on the root |
| [data-scrollable] | "start" | "end" | "start end" | absent, mirrored with the viewport |
| [data-autoscrolling] | Present while the viewport is programmatically scrolling to the live edge, mirrored with the viewport |
Viewport
The scroll container: a labelled, keyboard-focusable scroll region (role=region, aria-label=Messages, tabindex=0, all overridable through the attribute seam). Give it a vertical overflow and a bounded size. Renders a div.
| Prop | Type | Default |
|---|---|---|
| PreserveScrollOnPrepend | bool | true |
| ChildContent | RenderFragment? | - |
| …attributes | IDictionary<string, object>? | - |
Data attributes
| Data attribute | Values |
|---|---|
| [data-navius-messagescroller-viewport] | Present on the viewport |
| [data-scrollable] | "start" | "end" | "start end" | absent, mirrored with the root |
| [data-autoscrolling] | Present while the viewport is programmatically scrolling to the live edge, mirrored with the root |
Content
The transcript container: a live region (role=log, aria-relevant=additions) so appended rows announce without narrating every streamed token. Every direct child must be an Item. Renders a div.
| Prop | Type | Default |
|---|---|---|
| SpacerClass | string? | - |
| ChildContent | RenderFragment? | - |
| …attributes | IDictionary<string, object>? | - |
Data attributes
| Data attribute | Values |
|---|---|
| [data-navius-messagescroller-content] | Present on the transcript container |
| [data-navius-messagescroller-spacer] | Present on the internal, engine-sized spacer that makes room for anchored rows; style it via SpacerClass |
Item
One transcript row boundary: a message, a marker, a typing indicator, a separator or a load-earlier row. Renders a div.
| Prop | Type | Default |
|---|---|---|
| MessageId | string? | - |
| ScrollAnchor | bool | false |
| ChildContent | RenderFragment? | - |
| …attributes | IDictionary<string, object>? | - |
Data attributes
| Data attribute | Values |
|---|---|
| [data-navius-messagescroller-item] | Present on every row |
| [data-message-id] | Mirrors MessageId |
| [data-scroll-anchor] | "true" | "false", mirrors ScrollAnchor |
Button
The scroll-to-start/end control. Context-aware: active only while the viewport can still scroll toward its direction; inert when it can't. Renders a button.
| Prop | Type | Default |
|---|---|---|
| Direction | string | "end" |
| Behavior | string | "smooth" |
| ChildContent | RenderFragment? | - |
| …attributes | IDictionary<string, object>? | - |
Data attributes
| Data attribute | Values |
|---|---|
| [data-navius-messagescroller-button] | Present on the button |
| [data-direction] | "start" | "end" |
| [data-active] | "true" | "false", whether the button can currently scroll; false also sets tabindex=-1 and inert |
Accessibility
The Viewport is a labelled, keyboard-focusable scroll region (role="region", aria-label="Messages", tabindex="0").
The Content is a live region (role="log", aria-relevant="additions") so appended rows announce without narrating every streamed token;
pass aria-busy through the attribute seam while a turn streams if announcements should wait for the completed row.
The Button is a real <button> that goes inert with tabindex="-1" when it cannot scroll in its direction, so it never becomes a ghost focus stop.
Keyboard interactions
When the Viewport (or a focusable child) is focused, the standard browser scroll keys apply: Arrow keys, Page Up / Page Down, and Home / End. Navius does not intercept these; the engine only reacts to them to release the streamed-reply follow.
| Key | Description |
|---|---|
| Space / Enter | When focus is on the Button, activates it and scrolls toward its configured direction. |