Search for a command to run...
Chat transcript scroller that follows streamed replies only while the reader is at the live edge, opens a saved thread where it should, and keeps the reader's place when content grows above or below them.
MessageScroller fills its parent, so give it a height-constrained flex column.
The behavior is the headless @shadcn/react message scroller; these parts add the Delphi
styling. Full prop, data-attribute, and hook reference:
shadcn Message Scroller.
| Part | Role |
|---|---|
MessageScrollerProvider | Headless root. autoScroll, defaultScrollPosition ("start" | "end" | "last-anchor"), scrollEdgeThreshold. |
MessageScroller | Styled frame. Lays out the viewport and controls. |
MessageScrollerViewport | The scroll element. Native overflow with a thin scrollbar and a stable gutter; hidden until the opening scroll lands. |
MessageScrollerContent | Transcript container (role="log"). Every direct child is measured as a row. |
MessageScrollerItem | Optional row wrapper. messageId enables jump-to and prepend preservation; scrollAnchor marks a turn boundary. |
MessageScrollerButton | Scroll-to-bottom control. Always mounted; fades out and goes inert when there is nothing below. |
Hooks: useMessageScroller (scrollToEnd, scrollToStart, scrollToMessage),
useMessageScrollerScrollable ({ start, end } — at the bottom is !end), and
useMessageScrollerVisibility.
Call scrollToEnd({ behavior: "smooth" }) for reader-initiated jumps (a send, a "jump to latest"
action). Follow-output is instant on purpose; do not animate it.
Trigger scrollToEnd yourself when only the content's padding grows (an expanding footer inset).
A content-box resize observer never sees padding.
Mark rows with scrollAnchor unless the product has decided new turns should settle near the
top of the viewport. That is a behavior change, not a polish pass.
Wrap it in ScrollArea. The primitive must own the scroll element; this is the one scroller
that renders a styled native scrollbar.