Skip to content

Message Scroller

MessageScroller is a managed scroll region for conversational transcripts. It anchors each newly appended turn near the top of the viewport, follows the live edge while content streams in but only while the reader is already there, and preserves the currently visible row when older messages are prepended. The scroll hot path runs entirely in the engine, so transcript rows never re-render on scroll or stream.

@using Navius.Primitives.Components.MessageScroller
<NaviusMessageScrollerProvider>
<NaviusMessageScroller>
<NaviusMessageScrollerViewport>
<NaviusMessageScrollerContent>
<NaviusMessageScrollerItem />
</NaviusMessageScrollerContent>
</NaviusMessageScrollerViewport>
<NaviusMessageScrollerButton />
</NaviusMessageScroller>
</NaviusMessageScrollerProvider>

Cascades the scroll options and the shared context to every descendant. Renders no DOM of its own.

Prop Type Default Description
AutoScroll bool false Follows new content while the reader is already at the live edge. Wheel, touch, keyboard scrolling, scrollbar drags, and explicit jumps release the follow.
DefaultScrollPosition string "end" Opening position on the first non-empty render: "start", "end", or "last-anchor".
ScrollEdgeThreshold double 8 Distance in px from either edge that still counts as being at it. Drives the scrollable-edge state and the button’s active state.
ScrollMargin double 0 Margin in px applied to the aligned edge for scroll targets and the visibility reading line.
ScrollPreviousItemPeek double 64 Extra margin added to ScrollMargin when a newly appended anchor row is positioned, so part of the previous item stays visible above it.
ChildContent RenderFragment? - The scroller’s parts.

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 Description
ChildContent RenderFragment? - The Viewport and Button.
Attributes IDictionary<string, object>? - Forwarded to the rendered div.

The scroll container: a labelled, keyboard-focusable scroll region. Give it a vertical overflow and a bounded size. Renders a div.

Prop Type Default Description
PreserveScrollOnPrepend bool true Keeps the first visible row stable when older rows are prepended, keyed on MessageId.
ChildContent RenderFragment? - The Content.
Attributes IDictionary<string, object>? - Forwarded to the rendered div. Overrides role, aria-label, or tabindex if supplied.

The transcript container: a live region so appended rows announce without narrating every streamed token. Every direct child must be an Item. Renders a div.

Prop Type Default Description
SpacerClass string? - Class applied to the internal, engine-sized spacer element that makes room for anchored rows.
ChildContent RenderFragment? - The transcript Items.
Attributes IDictionary<string, object>? - Forwarded to the rendered div. Pass aria-busy here while a turn streams if announcements should wait for the completed row.

One transcript row boundary: a message, a marker, a typing indicator, a separator, or a load-earlier row. Renders a div.

Prop Type Default Description
MessageId string? - Stable row id used by jump-to-message, visibility tracking, and prepend preservation.
ScrollAnchor bool false Marks the row as a turn boundary that anchors newly appended turns.
ChildContent RenderFragment? - The row’s content.
Attributes IDictionary<string, object>? - Forwarded to the rendered div.

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 Description
Direction string "end" Which edge the button scrolls toward: "end" or "start".
Behavior string "smooth" Scroll behavior for the jump: "smooth" or "auto".
ChildContent RenderFragment? - Button label or icon content.
Attributes IDictionary<string, object>? - Forwarded to the rendered button.
Part Attribute Description
Root data-scrollable "start" | "end" | "start end" | absent, mirrored with the Viewport.
Root data-autoscrolling Present while the viewport is programmatically scrolling to the live edge, mirrored with the Viewport.
Viewport data-scrollable "start" | "end" | "start end" | absent, mirrored with the Root.
Viewport data-autoscrolling Present while the viewport is programmatically scrolling to the live edge, mirrored with the Root.
Content data-navius-messagescroller-spacer Present on the internal, engine-sized spacer that makes room for anchored rows; style it via SpacerClass.
Item data-message-id Mirrors MessageId.
Item data-scroll-anchor "true" | "false", mirrors ScrollAnchor.
Button data-direction "start" | "end".
Button data-active "true" | "false", whether the button can currently scroll; false also sets tabindex="-1" and inert.
Key Behavior
Space / Enter When focus is on the Button, activates it and scrolls toward its configured direction.

The Viewport renders role="region", aria-label="Messages", and tabindex="0" (each overridable through Attributes). The Content renders role="log" and aria-relevant="additions" so appended rows announce without narrating every streamed token. The Button is a native button that goes inert with tabindex="-1" when it cannot scroll in its direction, so it never becomes a ghost focus stop. The browser’s own scroll keys (arrows, Page Up/Down, Home/End) apply when the Viewport or a focusable child has focus; Navius does not intercept them, it only reacts to them to release the streamed-reply follow. This mechanism is covered by the Playwright browser test suite and gated by axe-core in CI.