Skip to content

[Virtualization] Visible content does not shift when in-DOM items above the viewport change height#65951

Merged
ilonatommy merged 17 commits intomainfrom
viewport-anchoring
Mar 30, 2026
Merged

[Virtualization] Visible content does not shift when in-DOM items above the viewport change height#65951
ilonatommy merged 17 commits intomainfrom
viewport-anchoring

Conversation

@ilonatommy
Copy link
Copy Markdown
Member

@ilonatommy ilonatommy commented Mar 24, 2026

Adds viewport stability to the Virtualize component: visible items stay in place when content above the viewport changes height, and when items are prepended to the collection.

Problem

The Virtualize component disables the browser's native scroll anchoring (overflow-anchor: none) to prevent an infinite rendering loop. This means any height change above the viewport (item expansion, data updates, etc.) causes visible content to shift ("scroll jumps"). (see issue for more details)
Additionally, prepending items to the collection silently shifts what the user sees with no compensation.

Approach: Hybrid scroll anchoring

Uses browser-native CSS Scroll Anchoring where available, with manual JS compensation as fallback.

Why not pure manual compensation like other frameworks?
Every major virtual scroll library (TanStack Virtual, react-window, react-virtualized) uses manual scrollTop adjustment and requires the developer to handle prepend scenarios. Our hybrid approach gives:

  • Flash-free compensation via native anchoring (no SignalR round-trip needed)
  • Transparent prepend handling for Items (zero developer effort)
  • Safari/table support via manual fallback.

Why native anchoring can't work for <table> layout?
CSS Scroll Anchoring miscalculates positions when <tr> elements are anchor candidates, causing 3000-8000px jumps per scroll event. Tested 9 CSS approaches — all fail. The fix disables browser anchoring for tables.

Description

C# (Virtualize.cs) - prepend detection for Items:

  • Detects when items are inserted before the viewport by comparing the first loaded item reference across renders (ReferenceEquals + ElementAtOrDefault for O(1) access).
  • Adjusts _itemsBefore so spacerBefore grows, creating a real DOM shift that the browser (or manual path) compensates automatically.
  • Scoped to DefaultItemsProvider - custom ItemsProvider returns new instances per request, making reference comparison unreliable. We would have to add new API for developers to inform virtualization component that they prepended. Other frameworks either do not support it at all (TanStack) or have additional manual step that is considered "convoluted" even by the maintainers and it still causes table jumps (e.g. react-virtuoso and its firstItemIndex; react-window with scrollOffset adjustments; react-virtualized that requires calling scrollToPosition()).

JS (Virtualize.ts) - two compensation mechanisms:

  • Native path (Chrome/Firefox/Edge + non-table): Set overflow-anchor: none only on spacers. The browser anchors on rendered items and compensates scrollTop atomically during layout.
  • Manual path (tables + Safari): Disable browser anchoring on the scroll container. Track item heights via ResizeObserver; compensate scrollTop when items above the viewport resize.

Manual path details:

  • Always observe all rendered items between the spacers (not just during convergence).
  • Track item heights in a Map; on resize, compute the delta.
  • Compensate scrollTop when the resized item is entirely above the viewport.
  • Skip compensation during convergence to avoid fighting with convergence pinning.

Convergence changes:

  • End/Home keydown listener starts convergence immediately, before the browser updates scroll position. With native anchoring enabled the browser may prevent scrollTop from reaching the absolute extremity, so IO-based detection alone (onSpacerAfterVisible) is not sufficient.
  • IntersectionObserver callbacks are throttled via a pending-entries map to deduplicate rapid intersection events.
expansion-above-viewport.mp4

Fixes #65939

@ilonatommy ilonatommy added this to the 11.0-preview4 milestone Mar 24, 2026
@ilonatommy ilonatommy self-assigned this Mar 24, 2026
@ilonatommy ilonatommy requested a review from a team as a code owner March 24, 2026 15:45
Copilot AI review requested due to automatic review settings March 24, 2026 15:45
@ilonatommy ilonatommy added area-blazor Includes: Blazor, Razor Components feature-blazor-virtualization This issue is related to the Blazor Virtualize component labels Mar 24, 2026
@ilonatommy ilonatommy marked this pull request as draft March 24, 2026 15:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds manual scroll compensation to the Virtualize JS implementation so visible content doesn’t “jump” when in-DOM items above the viewport change height (with native scroll anchoring disabled), and introduces E2E coverage for expand/collapse scenarios.

Changes:

  • Track rendered item heights via ResizeObserver and adjust scrollTop when above-viewport items resize.
  • Always observe rendered items between spacers (not only during convergence) and simplify convergence-related logic.
  • Add new E2E tests for expand/collapse-above-viewport stability; adjust the test asset to use more items for true off-screen virtualization.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/Components/Web.JS/src/Virtualize.ts Implements viewport anchoring via ResizeObserver height-delta tracking and scrollTop compensation.
src/Components/test/E2ETest/Tests/VirtualizationTest.cs Adds E2E tests validating visible-item stability when an above-viewport in-DOM item expands/collapses.
src/Components/test/testassets/BasicTestApp/VirtualizationDynamicContent.razor Increases item count to ensure the scenario exercises real virtualization.

Comment thread src/Components/test/E2ETest/Tests/VirtualizationTest.cs Outdated
Comment thread src/Components/Web.JS/src/Virtualize.ts
Comment thread src/Components/Web.JS/src/Virtualize.ts Outdated
Comment thread src/Components/test/E2ETest/Tests/VirtualizationTest.cs Outdated
@ilonatommy ilonatommy marked this pull request as ready for review March 25, 2026 10:28
@ilonatommy
Copy link
Copy Markdown
Member Author

ilonatommy commented Mar 25, 2026

Why not overflow-anchor with a workaround for infinite rendering loop, e.g. setting no anchor for spacers only? see Because overflow-anchor is not supported by Safari so it would limit the fix. Manual scroll works across all the browsers.

After digging a bit deeper, it looks like it's close to being added to stable version. It was added to stable in WebKit/WebKit#58575 but then taken back to preview in WebKit/WebKit@d123c18.

We should monitor https://bugs.webkit.org/show_bug.cgi?id=309279.

@javiercn
Copy link
Copy Markdown
Member

Why not overflow-anchor with a workaround for infinite rendering loop, e.g. setting no anchor for spacers only? see Because overflow-anchor is not supported by Safari so it would limit the fix. Manual scroll works across all the browsers.

After digging a bit deeper, it looks like it's close to being added to stable version. It was added to stable in WebKit/WebKit#58575 but then took back to preview in WebKit/WebKit@d123c18.

We should monitor https://bugs.webkit.org/show_bug.cgi?id=309279.

If it's on a stable release before November, we can use it. Our support policy is Last Version (-1), so if it lands in Safari 26.x, we are good.

Copy link
Copy Markdown
Member

@javiercn javiercn left a comment

Choose a reason for hiding this comment

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

Looks great and very thorough. Great job!

@ilonatommy ilonatommy merged commit fcf0610 into main Mar 30, 2026
25 checks passed
@ilonatommy ilonatommy deleted the viewport-anchoring branch March 30, 2026 07:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-blazor Includes: Blazor, Razor Components feature-blazor-virtualization This issue is related to the Blazor Virtualize component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Virtualize: visible content shifts when in-DOM items above the viewport change height

3 participants