Skip to content

paddingEnd option breaks followOnAppend + anchorTo: 'end' #1258

Description

@GianlucaGuarini

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

  • I agree to follow this project's Code of Conduct
  • I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.

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