Skip to content

[bug]: MessageScroller: autoScroll silently disengages at the clamped bottom (trailing wheel momentum, nested scrollers) and never re-arms #11224

Description

@doneumark

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

  1. Open the streaming demo on the Message Scroller docs page (it has autoScroll enabled) and send a message.
  2. 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.
  3. 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

  • I've made research efforts and searched the documentation
  • I've searched for existing issues

cc @shadcn

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions