Skip to content

ScrollToItemAsync(int itemIndex) design for Virtualization #66328

@ilonatommy

Description

@ilonatommy

Summary

Add a ScrollToItemAsync(int itemIndex) method and an InitialItemIndex parameter to the Virtualize component, enabling programmatic scrolling to any item by index and setting the initial first-visible item without flash-of-wrong-content. Scroll should be instant (no animations).

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: table-of-contents navigation in document viewers, return-to-position after edit-and-back-navigate, chat jumping to a specific message, setting initial scroll position without visible flash-of-wrong-content.

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. InitialItemIndex parameter — sets the initial scroll position on first render (latched once, ignored on re-renders). Implemented as a deferred internal call to ScrollToItemAsync once the item count is known; adds no new JS code path beyond what ScrollToItemAsync already needs.
  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

  • Reading the current first-visible item index. No known use cases for that; omitting it keeps the design free of any new [JSInvokable] surface (no JS→.NET pushes).
  • ScrollToItemAsync(TItem item) overload. Every concrete scenario in Virtualize component Row Index Enhancements (API to scroll to item) #26943 already has the index in hand; an item overload would also be unimplementable for ItemsProvider without an extra IndexResolver callback.
  • Smooth-scroll animation. Instant scroll only in v1; would interact poorly with convergence loops and IO suppression timing.

API surface

// New method
public Task ScrollToItemAsync(int itemIndex, CancellationToken cancellationToken = default);

// New parameter — sets the initial scroll position on first render
// (latched once; subsequent changes are ignored). Runtime scrolls go through
// ScrollToItemAsync. Implemented internally as a deferred call to ScrollToItemAsync
// once the item count is known.
[Parameter]
public int? InitialItemIndex { get; set; }

Edge case behavior

  • InitialItemIndex is set to value higher than _itemCount returned by the itemProvider -> use _itemCount - 1 (last item)
  • InitialItemIndex is set to negative value -> treat it as 0. We want to avoid throwing in runtime for incorrect values.
  • ScrollToItemAsync(100) is mid-flight; consumer calls ScrollToItemAsync(200) -> the last call wins, cancel previos calls.
  • User scrolls during the ScrollToItemAsync is in flight -> programmatic scroll wins.
  • Prepend/append happens when we scroll to index -> the index is literal at call time but re-clamped on mutation.

Considered alternatives

  1. Always rendering each item wrapped in a marker element (e.g., <div data-virtualize-item="{index}">) — rejected as a breaking change to existing DOM structure (CSS selectors, invalid HTML inside <table>/<tbody>, accessibility roles) with per-render overhead paid by every consumer. Unnecessary given that sibling-walk by target - _itemsBefore already gives JS a deterministic handle without any wrapper.

  2. Wrapping items only during scroll-to-item operations (soft version of point 1) — rejected. Sibling-walk by DOM child index (matching restoreAnchorForShift) makes the wrapper unnecessary. Avoiding the wrapper removes the CSS-selector breakage (>, +, ~, :nth-child), accessibility-tree concerns, and table validity issues that would otherwise apply.

  3. Dual-purpose FirstVisibleItemIndex parameter (setter + getter, with FirstVisibleItemIndexChanged event) — filed issues shows users want set capabilities (scroll-to, initial position), not real-time read of the value which would be complex due to timing of SignalR messages.

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
No fields configured for Feature.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions