Describe the bug
With autoScroll on, follow-bottom can turn itself off while the reader is sitting at the live edge, and after that the transcript stops following the stream for good — the scroll-to-end button becomes the only way to re-engage it.
The cause: MessageScrollerViewport treats every wheel / touchmove as scroll-away intent (userScrollIntent releases follow unconditionally, the direction is never checked), while re-arming only happens in reconcileFollowMode, which needs a scroll event to run. So any wheel event that can't actually move the viewport releases follow with no re-arm opportunity:
- Scroll up briefly during a stream, then flick back to the bottom. The viewport clamps at max
scrollTop and follow re-arms — but the trackpad keeps delivering trailing momentum wheel events with deltaY > 0. Each one releases follow again, and since the viewport can't move past the clamp, no scroll event ever reconciles it. The next streamed chunk grows the content past scrollEdgeThreshold and the re-arm condition can never become true again. You're left unpinned while visually sitting at the bottom.
- The same happens without ever leaving the bottom: a wheel consumed by a nested scrollable inside a message (an overflowing code block), or a horizontal two-finger pan (
deltaY === 0), bubbles to the viewport and releases follow while moving nothing.
It also feels flaky rather than deterministic: a chunk smaller than scrollEdgeThreshold still measures as "at the end" on the next commit and accidentally re-arms follow, while a bigger chunk doesn't. In a real chat this reads as "sometimes it re-sticks, usually it doesn't". I hit it in a chat app where assistant replies contain scrollable code blocks — panning inside one kills the stickiness every time.
I've submitted a fix in #11223: a wheel toward the end doesn't count as scroll-away while following, and a release out of follow schedules a state commit so a gesture that moved nothing reconciles back to following. It includes two Chromium regression tests that fail on main.
Related but distinct from #11125 — that one is about a manual scroll during the anchored phase; this one is about the following phase, and the fix doesn't touch anchored/settling release semantics.
Affected component/components
MessageScroller
How to reproduce
- Open the streaming demo on the Message Scroller docs page (it has
autoScroll enabled) and send a message.
- While the reply streams and the viewport follows the bottom, scroll up a little, then flick back down to the bottom with a trackpad and let the momentum run out.
- The viewport sits at the bottom but no longer follows: the next chunks render below the fold and the transcript stays put until you click the scroll button.
Variant that never leaves the bottom: while following, do a horizontal two-finger pan over the transcript (or wheel inside a nested scrollable in a message) — follow is released the same way even though the viewport never moved.
Codesandbox/StackBlitz link
https://ui.shadcn.com/docs/components/base/message-scroller (the streaming demo there reproduces it directly with a trackpad)
Logs
System Info
macOS, Chrome (also reproduced in headless Chromium via the package's own browser test setup)
"@shadcn/react": "0.2.1"
Before submitting
cc @shadcn
Describe the bug
With
autoScrollon, follow-bottom can turn itself off while the reader is sitting at the live edge, and after that the transcript stops following the stream for good — the scroll-to-end button becomes the only way to re-engage it.The cause:
MessageScrollerViewporttreats everywheel/touchmoveas scroll-away intent (userScrollIntentreleases follow unconditionally, the direction is never checked), while re-arming only happens inreconcileFollowMode, which needs ascrollevent to run. So any wheel event that can't actually move the viewport releases follow with no re-arm opportunity:scrollTopand follow re-arms — but the trackpad keeps delivering trailing momentum wheel events withdeltaY > 0. Each one releases follow again, and since the viewport can't move past the clamp, no scroll event ever reconciles it. The next streamed chunk grows the content pastscrollEdgeThresholdand the re-arm condition can never become true again. You're left unpinned while visually sitting at the bottom.deltaY === 0), bubbles to the viewport and releases follow while moving nothing.It also feels flaky rather than deterministic: a chunk smaller than
scrollEdgeThresholdstill measures as "at the end" on the next commit and accidentally re-arms follow, while a bigger chunk doesn't. In a real chat this reads as "sometimes it re-sticks, usually it doesn't". I hit it in a chat app where assistant replies contain scrollable code blocks — panning inside one kills the stickiness every time.I've submitted a fix in #11223: a wheel toward the end doesn't count as scroll-away while following, and a release out of follow schedules a state commit so a gesture that moved nothing reconciles back to following. It includes two Chromium regression tests that fail on
main.Related but distinct from #11125 — that one is about a manual scroll during the anchored phase; this one is about the following phase, and the fix doesn't touch anchored/settling release semantics.
Affected component/components
MessageScroller
How to reproduce
autoScrollenabled) and send a message.Variant that never leaves the bottom: while following, do a horizontal two-finger pan over the transcript (or wheel inside a nested scrollable in a message) — follow is released the same way even though the viewport never moved.
Codesandbox/StackBlitz link
https://ui.shadcn.com/docs/components/base/message-scroller (the streaming demo there reproduces it directly with a trackpad)
Logs
System Info
Before submitting
cc @shadcn