You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(virtual-core): iOS momentum-safe scroll adjustments via CSS deviation
On iOS WebKit, writing scrollTop during momentum scroll cancels the
in-flight scroll. Instead of writing scrollTop, apply a negative
marginTop on the container element to visually compensate for
above-viewport size changes. The deviation is flushed to a real
scrollTop write once momentum fully settles.
Changes:
- Add CSS deviation (marginTop) approach for iOS scroll adjustments
- Defer adjustments through touch→momentum→settled lifecycle
- Force-flush deviation before programmatic scroll operations
- Compensate scrollOffset for deviation in range calculations
- Guard against Safari elastic overscroll during flush
- Clean up CSS deviation on unmount
- Refine backward-scroll suppression: first measurements always
adjust (needed for prepend), re-measurements skip during backward
scroll (avoids scrollTop cascade jank)
- Add overflow-anchor: none to chat example CSS
- Update chat docs with iOS section and getItemKey best practices
feat: iOS momentum-safe scroll adjustments via CSS deviation
6
+
7
+
On iOS WebKit, writing `scrollTop` during momentum scroll cancels the in-flight scroll. Instead of writing `scrollTop`, we now apply a negative `marginTop` on the container element to visually compensate for above-viewport size changes. The deviation is flushed to a real `scrollTop` write once momentum fully settles.
8
+
9
+
- Defer scroll adjustments during iOS touch and momentum phases using CSS deviation (`marginTop`/`marginLeft`)
10
+
- Force-flush deviation before programmatic scroll operations (`scrollToIndex`, `scrollToOffset`, `scrollBy`)
11
+
- Compensate `scrollOffset` for active deviation in range calculations
12
+
- Guard against Safari elastic overscroll (rubber-band) during flush
13
+
- Clean up CSS deviation on unmount
14
+
- Refine backward-scroll suppression: first measurements always adjust regardless of direction; re-measurements skip during backward scroll to avoid the `scrollTop` cascade jank
Wrap `getItemKey` in `useCallback` so the virtualizer sees a stable reference between renders. Without it, every render creates a new function and the virtualizer cannot detect which edge keys changed.
59
+
52
60
Do not use index keys for chat history. After a prepend, every existing message shifts to a new index, so index keys cannot identify the same message across the update.
53
61
54
62
### Follow appended output only when pinned
@@ -127,14 +135,23 @@ Use a normal scroll container and normal item order. You do not need `flex-direc
127
135
128
136
## Production Checklist
129
137
130
-
- Use stable message ids with `getItemKey`.
138
+
- Use stable message ids with `getItemKey`, wrapped in `useCallback`.
131
139
- Give the scroll element a fixed height and `overflow: auto`.
140
+
- Set `overflow-anchor: none` on the scroll element. Browsers that support native scroll anchoring (Chrome, Firefox) will otherwise fight the virtualizer's own offset adjustments on prepend, causing jumps. Safari does not support `overflow-anchor`, so this has no effect there.
132
141
- Call `measureElement` for dynamic message heights.
133
142
- Use `anchorTo: 'end'` for prepend stability and streaming bottom growth.
134
143
- Use `followOnAppend` when new output should follow only from the latest position.
135
144
- Use `isAtEnd()` to show "Jump to latest" UI when the user is reading history.
136
145
- Keep network loading state outside the virtualizer; prepend or append data normally.
137
146
147
+
## iOS Safari
148
+
149
+
iOS WebKit cancels momentum (inertia) scrolling whenever JavaScript writes to `scrollTop` or calls `scrollTo()`. This is a platform limitation — there is no opt-out. Since prepend anchoring and item-resize compensation both need to adjust the scroll position, a naïve implementation would kill the scroll mid-flick, making the list feel broken on iPhones and iPads.
150
+
151
+
TanStack Virtual works around this with **CSS deviation**: when a scroll adjustment is needed during an active touch or momentum phase, the virtualizer applies a negative `marginTop` on the container element instead of writing `scrollTop`. This shifts the content visually without touching the scroll position, so momentum continues uninterrupted. Once the scroll fully settles (no touch, no momentum, no elastic overscroll), the virtualizer flushes the accumulated CSS offset into a single `scrollTop` write and clears the margin.
152
+
153
+
No extra configuration is needed — this is handled automatically on iOS.
0 commit comments