Describe the bug
With anchorTo: 'end' and followOnAppend: true, appending a new item while trimming the oldest item in the same update moves an end-pinned viewport away from the bottom when the count stays unchanged.
The reproduction starts with 20 messages, each 50 px tall, in a 300 px viewport. Changing messages 1–20 to 2–21 keeps the total height at 1000 px, but moves scrollTop from 700 to 650, leaving a 50 px bottom gap. Appending without trimming follows correctly.
Your minimal, reproducible example
Live window-follow reproduction
Pinned source · Source and local run instructions
Published packages: @tanstack/react-virtual@3.14.11, @tanstack/virtual-core@3.17.9, and React 19.2.8. Message IDs are persistent, and the memoized getItemKey callback captures each immutable message array. Native CSS scroll anchoring is disabled. There is no streaming, network, dynamic measurement, or custom scroll compensation.
Steps to reproduce
- Open the page and confirm Actual bottom gap: 0.00 px.
- Click Append + trim oldest once. The array changes from messages 1–20 to 2–21 in one state update.
- Observe Actual bottom gap: 50.00 px. The latest message is below the viewport.
- Click Reset to repeat.
- As a control, reset and click Append only (control). The count grows to 21 and the bottom gap remains 0 px.
Expected behavior
A viewport already within scrollEndThreshold of the end should stay at the end when new items are appended and older items are trimmed in the same update. In this example scrollTop should remain 700 and the bottom gap should remain 0.
A user reading earlier content should retain their reading position. For these non-growing updates, a replacement, reorder, or trim without new appended items should not be treated as appended output merely because the last key changes. This report does not request a change to the existing count-growing classification.
How often does this bug happen?
Every time with the steps above.
Screenshots or Videos

Measured from the real scroll element:
| Scenario |
Count |
scrollHeight |
clientHeight |
scrollTop |
Bottom gap |
| Initial |
20 |
1000 |
300 |
700 |
0 |
| Append + trim |
20 |
1000 |
300 |
650 |
50 |
| Append only, after reset |
21 |
1050 |
300 |
750 |
0 |
Platform
- macOS 26.5.2
- Chrome 152 (reported user-agent major version)
- Verified on 2026-09-08 using a production Vite build
tanstack-virtual version
@tanstack/react-virtual@3.14.11 / @tanstack/virtual-core@3.17.9
TypeScript version
Not applicable; the reproduction uses JavaScript/JSX.
Additional context
Proposed fix: #1272.
Related: #1270 describes inconsistent lazy measurement keys with a stable callback. This reproduction uses a callback that captures each immutable array and still loses end following, so the two behaviors can be reproduced independently.
The core's append-follow condition in setOptions requires nextCount > prevCount. This update changes the edge keys without increasing the count, so it takes the visible-item anchoring path instead of following the new tail.
The reproduction retains an ordered, non-empty suffix of the old list and appends a new key. It does not rely on treating wholesale replacement or recycled keys as an append.
Terms & Code of Conduct
Describe the bug
With
anchorTo: 'end'andfollowOnAppend: true, appending a new item while trimming the oldest item in the same update moves an end-pinned viewport away from the bottom when the count stays unchanged.The reproduction starts with 20 messages, each 50 px tall, in a 300 px viewport. Changing messages 1–20 to 2–21 keeps the total height at 1000 px, but moves
scrollTopfrom 700 to 650, leaving a 50 px bottom gap. Appending without trimming follows correctly.Your minimal, reproducible example
Live window-follow reproduction
Pinned source · Source and local run instructions
Published packages:
@tanstack/react-virtual@3.14.11,@tanstack/virtual-core@3.17.9, and React 19.2.8. Message IDs are persistent, and the memoizedgetItemKeycallback captures each immutable message array. Native CSS scroll anchoring is disabled. There is no streaming, network, dynamic measurement, or custom scroll compensation.Steps to reproduce
Expected behavior
A viewport already within
scrollEndThresholdof the end should stay at the end when new items are appended and older items are trimmed in the same update. In this examplescrollTopshould remain 700 and the bottom gap should remain 0.A user reading earlier content should retain their reading position. For these non-growing updates, a replacement, reorder, or trim without new appended items should not be treated as appended output merely because the last key changes. This report does not request a change to the existing count-growing classification.
How often does this bug happen?
Every time with the steps above.
Screenshots or Videos
Measured from the real scroll element:
Platform
tanstack-virtual version
@tanstack/react-virtual@3.14.11/@tanstack/virtual-core@3.17.9TypeScript version
Not applicable; the reproduction uses JavaScript/JSX.
Additional context
Proposed fix: #1272.
Related: #1270 describes inconsistent lazy measurement keys with a stable callback. This reproduction uses a callback that captures each immutable array and still loses end following, so the two behaviors can be reproduced independently.
The core's append-follow condition in
setOptionsrequiresnextCount > prevCount. This update changes the edge keys without increasing the count, so it takes the visible-item anchoring path instead of following the new tail.The reproduction retains an ordered, non-empty suffix of the old list and appends a new key. It does not rely on treating wholesale replacement or recycled keys as an append.
Terms & Code of Conduct