Skip to content

ScrollToItemAsync(int itemIndex) design for Virtualization #66328

@ilonatommy

Description

@ilonatommy

Summary

Add a ScrollToItemAsync(int itemIndex) method and a FirstVisibleItemIndex parameter to the Virtualize<TItem> component, enabling programmatic scrolling to any item by index and reading the current first visible item index.

Motivation and goals

  • Virtualize has no public API to scroll to a specific item. This is the most-requested Virtualize feature (Virtualize component Row Index Enhancements (API to scroll to item) #26943).
  • Users are forced into fragile workarounds: JS interop to manually compute scroll offsets, which breaks unpredictably with variable-height items and virtualization window shifts.
  • Real-world scenarios blocked by this gap:
    • Map-pin-to-list sync (10K+ items, clicking a pin scrolls to the corresponding list card)
    • Table-of-contents navigation in document viewers
    • Return-to-position after edit-and-back-navigate
    • Chat/log viewers jumping to a specific message
    • Setting initial scroll position without visible flash-of-wrong-content

The infrastructure needed for this feature — anchor snapshotting, IO suppression, and convergence — is being introduced by #66262 (AnchorMode). This design reuses that mechanism for scroll-to-item.

In scope

  1. ScrollToItemAsync(int itemIndex) — programmatically scroll to any item by zero-based index. Works with both Items and ItemsProvider. Returns a Task that completes when the target item is fully rendered, precisely aligned, and all IO/resize cycles have settled.
  2. FirstVisibleItemIndex parameter — dual-purpose: setter sets the initial scroll position on first render (latched once, ignored on re-renders); getter returns the zero-based index of the first item currently visible in the viewport, reported from JS via the existing IO callbacks.
  3. Variable-height item support — the scroll uses actual DOM measurement, not pixel-offset estimation, for final alignment.
  4. Async ItemsProvider support — convergence handles placeholder → real item transitions.
  5. Race-safe rapid calls — monotonic request IDs ensure only the latest call wins.

Risks / unknowns

  • Multi-element templates: ChildContent can render multiple root DOM elements per item. Sibling-walking can't reliably identify item N. Mitigated by adding a temporary wrapper element with data-virtualize-item="{index}" during scroll-to-item operations (removed after convergence).
  • IO suppression scope: AnchorMode suppresses IntersectionObserver callbacks until user scroll. ScrollToItemAsync must auto-re-arm observers after alignment (viewport intentionally changed). Incorrect re-arm timing could cause feedback loops or stale distribution.
  • Server-side Blazor latency: Each convergence cycle is a C#→JS round-trip. Convergence will take more cycles but should complete correctly. Need to validate there's no visible flicker.
  • Convergence guard: If item height estimates are wildly off, convergence could loop. A max iteration count (e.g., 10 render cycles) with a forced fallback prevents this.

Detailed design

Two-stage scroll approach

Rather than manually setting internal _itemsBefore state (which bypasses distribution invariants), the design uses two stages:

Stage 1 — Estimated jump (JS-driven):
JS sets scrollTop to an estimated pixel offset (targetIndex × avgItemHeight). This triggers the normal IntersectionObserver cycle which naturally adjusts the rendered window to include the target index.

Stage 2 — Precise alignment (after render):
Once the target item is in the rendered DOM, JS locates it via a data-virtualize-item attribute, measures its actual position, and adjusts scrollTop to align it with the container's top. The Task completes at this point.

Reused infrastructure from AnchorMode:

Mechanism ScrollToItem use
updateAnchorSnapshot() Save target item position after precise alignment
ignoreAnchorScroll flag Skip synthetic scroll events from estimated jump and precise alignment
suppressSpacerCallbacks Suppress stale IO from estimated jump, auto-re-arm after alignment via microtask
Convergence observing Keep target item stable as async items load

Key difference: AnchorMode suppresses IO until next user scroll (viewport unchanged). ScrollToItem re-arms observers automatically after alignment because the viewport intentionally changed.

Race handling

A monotonic _scrollRequestId is incremented on each ScrollToItemAsync call. All JS calls include the ID. Stale operations are ignored in both C# (OnAfterRenderAsync) and JS (alignToItem). A new call cancels the previous TaskCompletionSource.

Multi-element templates

During scroll-to-item, BuildRenderTree wraps each item in a lightweight element with data-virtualize-item="{logicalIndex}". JS uses querySelector to find the target. The wrapper is removed after convergence completes.

API surface

// New method
public Task ScrollToItemAsync(int itemIndex);

// New parameter — setter: initial scroll position (latched once);
//                 getter: current first visible item index (updated from JS on scroll)
[Parameter]
public int? FirstVisibleItemIndex { get; set; }

Considered alternatives

  1. Directly setting _itemsBefore in C# — rejected because it bypasses UpdateItemDistribution invariants (clamping, skip-refresh logic, render sequencing), causing duplicate renders and mismatched state.

  2. DOM sibling-walking to find target item — rejected because ChildContent can render multiple root elements per item, making child index unreliable for identifying specific logical items.

  3. Always rendering wrapper elements — rejected as a breaking change to existing DOM structure with unnecessary performance overhead for the common case.

  4. Waiting for user scroll to re-arm IO (matching AnchorMode) — rejected because ScrollToItemAsync intentionally changes the viewport. Waiting would deadlock virtualization.

Metadata

Metadata

Assignees

Labels

area-blazorIncludes: Blazor, Razor Componentsdesign-proposalThis issue represents a design proposal for a different issue, linked in the descriptionfeature-blazor-virtualizationThis issue is related to the Blazor Virtualize componentfeature-request

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions