Describe the bug
When paddingEnd is set alongside anchorTo: 'end' and followOnAppend, the follow-on-append behaviour silently stops working. The virtualizer never scrolls to the end after new items are appended, even when the viewport was already pinned at the bottom.
The root cause is an internal inconsistency between two "distance from end" calculations:
getDistanceFromEnd() — used by isAtEnd() and the followOnAppend trigger in setOptions:
// reads real DOM scrollHeight - clientHeight
return Math.max(this.getMaxScrollOffset() - this.getScrollOffset(), 0)
getVirtualDistanceFromEnd() — used by resizeItem's end-anchor guard (wasAtEnd):
// uses getTotalSize(), which adds paddingEnd
return Math.max(this.getTotalSize() - this.getSize() - this.getScrollOffset(), 0)
getTotalSize() includes paddingEnd:
return Math.max(end - this.options.scrollMargin + this.options.paddingEnd, 0)
So getVirtualDistanceFromEnd() always reports a distance of at least paddingEnd pixels, even when the user is scrolled to the absolute bottom of the container.
The followOnAppend trigger in setOptions calls this.isAtEnd(prevOptions.scrollEndThreshold) — this uses the DOM-based path and correctly detects the pinned state. So followOnAppend is triggered and scrollToEnd() is scheduled.
But the wasAtEnd guard in resizeItem uses getVirtualDistanceFromEnd():
const wasAtEnd =
this.options.anchorTo === 'end' &&
this.scrollState?.behavior !== 'smooth' &&
this.getVirtualDistanceFromEnd() <= this.options.scrollEndThreshold
With e.g. paddingEnd: 80 and scrollEndThreshold: 80, getVirtualDistanceFromEnd() returns >= 80 even at the very bottom, so wasAtEnd is always false. The end-anchor scroll adjustment in resizeItem never fires while a streaming message is growing, the viewport drifts away from the bottom, and by the time scrollToEnd() runs it either undershoots or the user is already well above the end.
Your minimal, reproducible example
https://github.com/tanstack/virtual/tree/main/examples/react/chat
Steps to reproduce
To reproduce the issue simply load the official example https://tanstack.com/virtual/latest/docs/framework/react/examples/chat adding the paddingEnd option
const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 74,
getItemKey: React.useCallback(
(index: number) => messages[index]!.id,
[messages],
),
anchorTo: 'end',
followOnAppend: true,
paddingEnd: 80,
scrollEndThreshold: 80,
overscan: 6,
directDomUpdates: true,
})
Now by streaming the response you will see that the chat will not scroll to the end when new content gets appended.
Expected behavior
When paddingEnd > 0 and the viewport is pinned at the bottom, followOnAppend should still follow appended items to the end. The two "distance from end" methods should be consistent.
The minimal fix in getVirtualDistanceFromEnd() would be to subtract paddingEnd:
private getVirtualDistanceFromEnd = () => {
return Math.max(
this.getTotalSize() - this.options.paddingEnd - this.getSize() - this.getScrollOffset(),
0,
)
}
How often does this bug happen?
None
Screenshots or Videos
No response
Platform
- OS: macOS / Linux
- Browser: Chrome, Firefox, Safari
tanstack-virtual version
@tanstack/react-virtual@3.14.10
TypeScript version
No response
Additional context
Related issue #1257
Terms & Code of Conduct
Describe the bug
When
paddingEndis set alongsideanchorTo: 'end'andfollowOnAppend, the follow-on-append behaviour silently stops working. The virtualizer never scrolls to the end after new items are appended, even when the viewport was already pinned at the bottom.The root cause is an internal inconsistency between two "distance from end" calculations:
getDistanceFromEnd()— used byisAtEnd()and thefollowOnAppendtrigger insetOptions:getVirtualDistanceFromEnd()— used byresizeItem's end-anchor guard (wasAtEnd):getTotalSize()includespaddingEnd:So
getVirtualDistanceFromEnd()always reports a distance of at leastpaddingEndpixels, even when the user is scrolled to the absolute bottom of the container.The
followOnAppendtrigger insetOptionscallsthis.isAtEnd(prevOptions.scrollEndThreshold)— this uses the DOM-based path and correctly detects the pinned state. SofollowOnAppendis triggered andscrollToEnd()is scheduled.But the
wasAtEndguard inresizeItemusesgetVirtualDistanceFromEnd():With e.g.
paddingEnd: 80andscrollEndThreshold: 80,getVirtualDistanceFromEnd()returns>= 80even at the very bottom, sowasAtEndis alwaysfalse. The end-anchor scroll adjustment inresizeItemnever fires while a streaming message is growing, the viewport drifts away from the bottom, and by the timescrollToEnd()runs it either undershoots or the user is already well above the end.Your minimal, reproducible example
https://github.com/tanstack/virtual/tree/main/examples/react/chat
Steps to reproduce
To reproduce the issue simply load the official example https://tanstack.com/virtual/latest/docs/framework/react/examples/chat adding the
paddingEndoptionNow by streaming the response you will see that the chat will not scroll to the end when new content gets appended.
Expected behavior
When
paddingEnd > 0and the viewport is pinned at the bottom,followOnAppendshould still follow appended items to the end. The two "distance from end" methods should be consistent.The minimal fix in
getVirtualDistanceFromEnd()would be to subtractpaddingEnd:How often does this bug happen?
None
Screenshots or Videos
No response
Platform
tanstack-virtual version
@tanstack/react-virtual@3.14.10
TypeScript version
No response
Additional context
Related issue #1257
Terms & Code of Conduct