Nov 2, 2026 · Reflection · 6 min
Designing the scroll
You’re reading the third line of an answer. The model is writing the fortieth. Somewhere between the two, the interface has to decide who owns the scroll position, and it usually decides wrong.
On the rest of the web the page holds still until you move it. Streaming breaks that: the content grows while you read, and every interface has to choose between following the text and leaving you where you are. Most choose following, then fight the reader for the next thirty seconds.
None of this can be decided in a mockup. A frame has no scroll position, no stream, no thumb on the glass. So I built the behaviour first and wrote the rules after. Here they are, with the demos I used to decide.
Who is scrolling?
The scroll event fires the same way whether the reader moved or your code did. It tells you the position changed. It can’t tell you who changed it.
Intent lives in other events: wheel, touchmove, Page Up, Space, a drag on the scrollbar. Only a person produces those. Listen to them to decide the reader has taken over, and treat scroll as a measurement, never as a signal.
The bug I see most often comes from skipping this. The interface checks “am I at the bottom?” on every scroll event while its own smooth scroll is still on its way to where the end was. The next burst moves the end, the check says no, and the interface concludes the reader scrolled away. Its animation still finishes, a little further down, at an end that is no longer there. Then nothing restarts it. Nobody touched anything.
The panel on the left asks a different question: has the reader made a gesture? As long as the answer is no, it keeps following, however far ahead the text gets for a moment.
RecommendedListens for intent
Not recommendedChecks position on scroll
Where following stops
Following should stop the moment the reader leaves, and start again when they come back. The two directions don’t use the same rule. Leaving is decided by intent: the reader scrolls up, following stops, whatever the distance. Coming back is decided by position: the reader scrolls back down to the bottom, following resumes.
Which leaves defining “the bottom”. Not zero: scrollTop is fractional on high-density screens and content grows between two frames, so an exact comparison fails, and following never comes back.
Not five lines either: a reader heading down to finish rereading is caught and carried to the end before they are done. The bottom is a band of one line: come back within a line of the end and you are at the end.
Not recommended0 px
Recommended1 line
Not recommended5 lines
Hold the line you’re reading
Once the reader has let go, the rule flips. The line they are reading must not move. And things above it keep growing: an image without dimensions, a table filling up, a citation that expands when its source resolves.
Browsers already have a mechanism for this. Scroll anchoring, controlled by overflow-anchor, picks a node in view and keeps it still when content above it changes size.1 Chrome has had it since 56, Firefox since 66. Safari has never shipped it, on the desktop or on iOS.
It isn’t enough on its own. The anchor holds the top of the viewport, not the line you’re reading, so anything that grows between the two still pushes that line down. Reserve the space you already know about: dimensions on every image, the size of a citation card. The anchor is a safety net, not a layout, and on iOS there is no net.
Not recommendedBrowser anchoring
RecommendedReserved space
Or don’t follow at all
There is another answer, and more chat interfaces are moving to it: don’t follow. Put the question at the top of the viewport and let the answer fill the space below.
The reader lands at the top of the answer, where reading starts anyway. For the first screen there is nothing to follow. Past it, they scroll at their own pace.
Either way you need a way back down. A “new content below” control that appears only when something landed out of view, sits just above the composer, and says what arrived, not just an arrow.
RecommendedLets go on scroll
RecommendedPins the question
The keyboard is a scroll event
On a phone the viewport isn’t fixed. The keyboard opens and the visible area loses between a quarter and a half of its height, depending on the device, the language and whether the suggestion bar is up. There is no number to hard-code, and the last line of the answer disappears behind the composer.
Listen to visualViewport, not window: it is the one that knows. And declare how the page should respond to the keyboard with interactive-widget in the viewport meta tag.2 Chrome has honoured it on Android since 108. iOS Safari ignores it, which is why the first sentence is the one that matters.
Then test it on a device. A desktop browser in mobile mode has no keyboard.
RecommendedListens to visualViewport
Not recommendedListens to window
The numbers I use
| Constant | Value | Why this value |
|---|---|---|
DETACH_ON | wheel · touch · keys · scrollbar | only a person produces them |
BOTTOM_BAND | 1 line | within a line of the end is the end |
REATTACH_ON | inside BOTTOM_BAND | intent to leave, position to return |
FOLLOW_SCROLL | a glide that re-aims every frame | a smooth scroll aimed at where the end was gets left 353 px behind; one that re-aims never does |
JUMP_PILL | only when content lands out of view | no new content, no button |