Chat scrollers that follow streams without yanking the reader
A React chat scroller that follows streaming messages near the bottom, freezes once readers scroll up, and gives them accessible ways to jump back down.
This article is published in English.
Sources: Building a chat interface that doesn’t fight the reader
The scrolling decisions behind beUI’s MessageScroller.
You scroll upward to re-read a sentence. The assistant is still generating. Another token chunk lands, and the viewport jumps back to the latest line.
You scroll up again.
Polished bubbles do not rescue a chat UI that fights the reader. That friction is one of the interaction details addressed while building beUI (beui.dev), a React component library focused on motion and interaction.
MessageScroller’s design question is narrow: when should the viewport chase the streaming reply, and when should it stay put?
Following is a state
A naïve approach scrolls to the bottom on every message mutation. That feels fine while watching the newest answer. It collapses the moment someone inspects earlier context.
Treat the situations as distinct states:
- Following: the reader is near the bottom, so growing content may advance the viewport.
- Reading earlier messages: the reader has left the bottom, so new output must not yank them downward.
Distance from the bottom of the scrollable region drives the decision inside beUI:
const distanceFromEnd =
viewport.scrollHeight -
viewport.scrollTop -
viewport.clientHeight;
const following = distanceFromEnd <= followThreshold;
The default threshold is 56 pixels—a tunable slack band near the bottom so tiny movements do not disable following immediately.
Exact equality is intentionally avoided. Scroll offsets can be fractional even when height metrics look rounded—browser scrollHeight behaviour documented on MDN calls this out.
Watch the content grow
Streaming often mutates an existing message instead of appending a new row. Message count can stay constant while a paragraph gains several lines.
beUI watches the content container with ResizeObserver. On size changes it re-checks whether following is still active before scrolling to the end.
That couples behavior to rendered geometry rather than assuming every chunk alters layout. Image loads or expanded tool results that change height are noticed the same way—ResizeObserver exists specifically to report element dimension changes.
Limiting automatic scroll does not freeze the document. An earlier image without reserved space can still shove later text. Stable media sizing and predictable message layout remain part of the product work.
The reader needs to interrupt
Programmatic scrolls emit scroll events too. Those must not be misread as the user choosing a new position.
MessageScroller tracks whether it is scrolling itself. Wheel input, touch gestures, and keys such as Page Up clear that guard so subsequent events can update the following flag based on true reader motion.
The distinction matters while tokens arrive continuously. Fresh chunks should update the answer text; they are not a warrant to steal focus from a paragraph someone is reading.
Make returning deliberate
After the reader leaves the bottom, returning should be intentional.
MessageScroller can render a compact navigation rail over Message rows. Each marker carries a text preview so long threads are scannable without scrubbing the full transcript.
Choosing an earlier marker disables following and scrolls toward that message. Choosing the last marker returns to the end and re-enables following. Manually scrolling back inside the bottom threshold also restores following.
The component emits onFollowChange so apps can show their own “Back to latest” control. That control is an application concern; the rail is the built-in navigator.
Smooth should still be optional
Smooth following is on by default. Consumers can disable it with the smooth prop, and the component falls back to immediate scrolling when reduced motion is requested.
Either mode should stay mentally simple: follow at the bottom, allow reading above it, resume when the reader opts in.
Viewport motion is not a substitute for accessibility signaling. Expose the transcript as a log-style live region with polite updates and a busy flag; validate the pairing against your renderer and screen reader rather than assuming defaults suffice.
Test the moment someone scrolls away
A short finished dialogue will not stress this logic. Prefer a long reply that is still streaming:
- Stay pinned to the latest output and ensure fresh lines remain visible.
- Move upward while tokens still arrive and verify the viewport is not dragged down.
- Jump via keyboard and the message rail to older turns without losing place.
- Come back to the newest message and check that following turns on again.
- Re-run the same checks with reduced motion enabled and with content whose height changes (images, tool panels).
Those checks are part of what makes an agent UI feel considerate: generation can continue while attention stays where the reader placed it.
The MessageScroller demo and source live under the agents section of the beUI site. Send a demo message, then scroll upward while the reply streams in.