feat(virtual-core): iOS momentum-safe scroll adjustments via CSS offset by piecyk · Pull Request #1189 · TanStack/virtual · GitHub
Skip to content

feat(virtual-core): iOS momentum-safe scroll adjustments via CSS offset - #1189

Closed
piecyk wants to merge 1 commit into
TanStack:mainfrom
piecyk:damian/feat/ios-momentum-safe-scroll-adjustments
Closed

piecyk wants to merge 1 commit into
TanStack:mainfrom
piecyk:damian/feat/ios-momentum-safe-scroll-adjustments

Conversation

@piecyk

@piecyk piecyk commented Jun 8, 2026 •

Copy link
Copy Markdown
Collaborator

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 CSS offset is flushed to a real scrollTop write once momentum fully settles.

  • Defer scroll adjustments during iOS touch and momentum phases using CSS offset (marginTop/marginLeft)
  • Force-flush CSS offset before programmatic scroll operations (scrollToIndex, scrollToOffset, scrollBy)
  • Compensate scrollOffset for active CSS offset in range calculations
  • Guard against Safari elastic overscroll (rubber-band) during flush
  • Clean up CSS offset on unmount
  • Refine backward-scroll suppression: first measurements always adjust regardless of direction; re-measurements skip during backward scroll to avoid the scrollTop cascade jank

🎯 Changes

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Improvements

    • Smoother iOS Safari momentum scrolling via deferred CSS-based offset compensation and safer flush handling.
    • Programmatic scrolling now forces resolution of any active iOS deferred offsets before performing scrolls.
  • Behavior

    • Refined resize-scroll adjustments: always apply the first backward-scroll measurement, skip redundant re-measurement adjustments, and keep offset calculations consistent during deferral.
  • Documentation

    • Chat/virtualization guidance now requires stable item keys and recommends disabling scroll anchoring.
  • Tests

    • Updated tests to match refined forward/backward resize-adjustment behavior.

@piecyk
piecyk requested a review from tannerlinsley June 8, 2026 06:33
@coderabbitai

coderabbitai Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

@nx-cloud

nx-cloud Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit dfe7dc9

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 2m 36s View ↗
nx run-many --target=build --exclude=examples/** ✅ Succeeded 17s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-06-08 15:23:42 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jun 8, 2026 •

Copy link
Copy Markdown
More templates

@tanstack/angular-virtual

npm i https://pkg.pr.new/@tanstack/angular-virtual@1189

@tanstack/lit-virtual

npm i https://pkg.pr.new/@tanstack/lit-virtual@1189

@tanstack/react-virtual

npm i https://pkg.pr.new/@tanstack/react-virtual@1189

@tanstack/solid-virtual

npm i https://pkg.pr.new/@tanstack/solid-virtual@1189

@tanstack/svelte-virtual

npm i https://pkg.pr.new/@tanstack/svelte-virtual@1189

@tanstack/virtual-core

npm i https://pkg.pr.new/@tanstack/virtual-core@1189

@tanstack/vue-virtual

npm i https://pkg.pr.new/@tanstack/vue-virtual@1189

commit: 1e6c48d

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/virtual-core/src/index.ts (1)

742-756: 💤 Low value

Consider edge case: scrollOffset is null when force-flushing.

In _forceFlushIosDeviation, if scrollOffset is null and _iosDeferredAdjustment is non-zero, rawBrowserScroll would compute as a negative value (0 - adjustment). While this scenario is unlikely in practice (deviation is only accumulated after scroll events set scrollOffset), a defensive guard would prevent unexpected behavior if the invariant is violated.

🛡️ Optional defensive guard
 private _forceFlushIosDeviation = () => {
   if (this._iosDeferredAdjustment === 0) return
+  if (this.scrollOffset === null) {
+    // No scroll offset yet; just clear the deviation state
+    this._iosDeferredAdjustment = 0
+    this._applyIosDeviation()
+    return
+  }
   const rawBrowserScroll =
     (this.scrollOffset ?? 0) - this._iosDeferredAdjustment
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/virtual-core/src/index.ts` around lines 742 - 756, If
this._iosDeferredAdjustment is non-zero but this.scrollOffset is null, computing
rawBrowserScroll yields an incorrect value; add a defensive guard at the top of
_forceFlushIosDeviation to handle null scrollOffset (e.g. if this.scrollOffset
== null then clear _iosDeferredAdjustment and call _applyIosDeviation and
return) so you don't call _scrollToOffset with an invalid rawBrowserScroll;
reference the method name _forceFlushIosDeviation and the fields scrollOffset,
_iosDeferredAdjustment and the helpers _applyIosDeviation and _scrollToOffset
when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/virtual-core/src/index.ts`:
- Around line 742-756: If this._iosDeferredAdjustment is non-zero but
this.scrollOffset is null, computing rawBrowserScroll yields an incorrect value;
add a defensive guard at the top of _forceFlushIosDeviation to handle null
scrollOffset (e.g. if this.scrollOffset == null then clear
_iosDeferredAdjustment and call _applyIosDeviation and return) so you don't call
_scrollToOffset with an invalid rawBrowserScroll; reference the method name
_forceFlushIosDeviation and the fields scrollOffset, _iosDeferredAdjustment and
the helpers _applyIosDeviation and _scrollToOffset when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e304110e-eb8c-4e87-8cfc-e899b4dc56ce

📥 Commits

Reviewing files that changed from the base of the PR and between 932c358 and d9919d9.

📒 Files selected for processing (5)
  • .changeset/ios-momentum-scroll.md
  • docs/chat.md
  • examples/react/chat/src/index.css
  • packages/virtual-core/src/index.ts
  • packages/virtual-core/tests/index.test.ts

@piecyk piecyk changed the title feat(virtual-core): iOS momentum-safe scroll adjustments via CSS devi… feat(virtual-core): iOS momentum-safe scroll adjustments via CSS deviation Jun 8, 2026
@piecyk piecyk changed the title feat(virtual-core): iOS momentum-safe scroll adjustments via CSS deviation feat(virtual-core): iOS momentum-safe scroll adjustments via CSS offset Jun 8, 2026
@piecyk
piecyk force-pushed the damian/feat/ios-momentum-safe-scroll-adjustments branch from d9919d9 to dfe7dc9 Compare June 8, 2026 15:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/chat.md`:
- Line 58: Update the rationale to explain that useCallback is applied to
getItemKey to stabilize its function reference so memoized values that include
getItemKey (such as getMeasurements which depends on
this.getMeasurementOptions()) don't treat measurement options as changed on
every render; specifically mention wrapping getItemKey in useCallback to keep a
stable reference, how passing a new getItemKey identity each render can
invalidate getMeasurements/getMeasurementOptions memoization and cause
unnecessary measurement rebuilds/cache invalidation, and note this keeps the
virtualizer from triggering extra recomputation while clarifying that edge-key
change detection itself is based on returned key values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 77b40f76-f2ad-4786-be9a-e5b0ed7c5cee

📥 Commits

Reviewing files that changed from the base of the PR and between d9919d9 and dfe7dc9.

📒 Files selected for processing (5)
  • .changeset/ios-momentum-scroll.md
  • docs/chat.md
  • examples/react/chat/src/index.css
  • packages/virtual-core/src/index.ts
  • packages/virtual-core/tests/index.test.ts
✅ Files skipped from review due to trivial changes (2)
  • examples/react/chat/src/index.css
  • .changeset/ios-momentum-scroll.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/virtual-core/src/index.ts
  • packages/virtual-core/tests/index.test.ts

Comment thread docs/chat.md Outdated
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 CSS offset is flushed to a real
scrollTop write once momentum fully settles.

Changes:
- Add CSS offset (marginTop) approach for iOS scroll adjustments
- Defer adjustments through touch→momentum→settled lifecycle
- Force-flush CSS offset before programmatic scroll operations
- Compensate scrollOffset for CSS offset in range calculations
- Guard against Safari elastic overscroll during flush
- Clean up CSS offset 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
@piecyk

piecyk commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant