Kebechet.Blazor.TanStack.VirtualCore 3.17.7.2

Prefix Reserved
dotnet add package Kebechet.Blazor.TanStack.VirtualCore --version 3.17.7.2
                    
NuGet\Install-Package Kebechet.Blazor.TanStack.VirtualCore -Version 3.17.7.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Kebechet.Blazor.TanStack.VirtualCore" Version="3.17.7.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Kebechet.Blazor.TanStack.VirtualCore" Version="3.17.7.2" />
                    
Directory.Packages.props
<PackageReference Include="Kebechet.Blazor.TanStack.VirtualCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Kebechet.Blazor.TanStack.VirtualCore --version 3.17.7.2
                    
#r "nuget: Kebechet.Blazor.TanStack.VirtualCore, 3.17.7.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Kebechet.Blazor.TanStack.VirtualCore@3.17.7.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Kebechet.Blazor.TanStack.VirtualCore&version=3.17.7.2
                    
Install as a Cake Addin
#tool nuget:?package=Kebechet.Blazor.TanStack.VirtualCore&version=3.17.7.2
                    
Install as a Cake Tool

"Buy Me A Coffee"

Blazor.TanStack.VirtualCore

NuGet Version NuGet Downloads Build codecov Storybook Last updated Twitter

Virtualization for Blazor lists whose items are genuinely different sizes, powered by @tanstack/virtual-core.

Every item is measured individually. Blazor keeps ownership of rendering - the JS side only answers "which indices are visible, and at what offsets", and the component renders that slice. Vertical, horizontal and multi-lane (masonry) layouts are all supported.

The virtual-core build is vendored and pinned inside the package, so there is no npm step, no CDN, and no <script> tag.

Live storybook - interactive stories for every feature.

Why not the built-in Virtualize?

The framework's Virtualize assumes one fixed ItemSize. .NET 11 adds variable-height support (dotnet/aspnetcore#65158), and for most lists that will be the right choice - it is in the box and needs no JS.

Reach for this package when the approximation is the problem:

Built-in Virtualize (.NET 11) This package
Row sizing weighted running average, no per-item storage exact measurement, cached per item key
Cache across item-set changes re-measured preserved per key, so a filter change keeps known heights
Content resize at a stable key manual RefreshDataAsync() targeted InvalidateItemSize(key)

If your rows differ by a little, take the built-in one. If a 60px row sits next to a 400px expanded one, an average places both wrong and the list visibly snaps as measurements arrive.

Installation

dotnet add package Kebechet.Blazor.TanStack.VirtualCore

No service registration is needed - the component imports its own interop module.

Usage

The component fills its nearest scrollable ancestor, which it finds by walking up for a computed overflow-y of auto or scroll, falling back to the document scroller.

@using Kebechet.Blazor.TanStack.VirtualCore

<div style="height: 100vh; overflow-y: auto;">
    <AdaptiveVirtualize TItem="Exercise"
                        Items="_exercises"
                        ItemKeyExtractor="@(x => x.Id.ToString())"
                        ItemSizeEstimator="@(x => x.IsExpanded ? 240 : 72)"
                        Options="_options">
        <ItemContent>
            <ExerciseRow Exercise="context" />
        </ItemContent>
    </AdaptiveVirtualize>
</div>

@code {
    private readonly VirtualizeOptions _options = new() { Overscan = 5 };
}

ItemKeyExtractor is load-bearing. It is both Blazor's @key and the measurement-cache key, so it must be stable for an item across renders and unique between items - otherwise measurements attach to the wrong item.

Options carries the whole upstream surface and is reactive: assigning a new instance pushes the changed values to the live virtualizer. Mutating the existing instance in place does nothing, because the component compares references.

Layouts

// A horizontal card rail
new VirtualizeOptions { IsHorizontal = true, Gap = 8 }

// A three-column masonry grid, balanced by measured size
new VirtualizeOptions { Lanes = 3, LaneAssignmentMode = LaneAssignmentMode.Measured, Gap = 10 }

// A log tail that follows new entries
new VirtualizeOptions { FollowOnAppend = FollowOnAppend.Instant, AnchorTo = ScrollAnchor.End }

// Virtualize against the document rather than a scrollable ancestor
new VirtualizeOptions { UseWindowScroller = true }

In a multi-lane layout each placement reports the Lane it landed in, and the component turns that into the item's cross-axis position and size - so your ItemContent only owns its own inner spacing. An outer margin would fight the computed lane width.

Keeping items rendered off-screen

new VirtualizeOptions { AlwaysRenderedIndexes = [0, 1] }

Those indices stay in the DOM however far away you scroll - a sticky header, a pinned selection, a row being dragged. This is upstream's rangeExtractor, reshaped: a JS predicate would cost an interop round-trip on every range computation, so the wrapper takes the indices and compiles the extractor itself.

Invalidating one row

When a row's content changes shape but its key does not - a group toggling collapsed to expanded - the preserved measurement is stale:

private AdaptiveVirtualize<Exercise>? _virtualize;

private async Task ToggleGroup(Exercise exercise)
{
    exercise.IsExpanded = !exercise.IsExpanded;
    await _virtualize!.InvalidateItemSize(exercise.Id.ToString());
}

Scrolling and inspecting

await _virtualize.ScrollToIndex(42, ScrollAlignment.Center, ScrollBehavior.Smooth);
await _virtualize.ScrollToOffset(2000);
await _virtualize.ScrollToEnd(ScrollBehavior.Instant);
await _virtualize.ScrollBy(300);
await _virtualize.ScrollToTop();

// Override a size directly, without waiting for the DOM to be measured
await _virtualize.ResizeItem(index: 0, size: 200);

// Live state: offset, total size, isScrolling, isAtEnd, direction, rendered range
var snapshot = await _virtualize.GetSnapshot();

// Save measurements, then restore them on a later mount to skip re-measuring
var measurements = await _virtualize.TakeSnapshot();

Two-phase rendering

Directional overscan

Overscan buffers both sides equally. OverscanLeading / OverscanTrailing spend more of that budget on the side the user is scrolling towards, which is where the next items are actually needed:

private readonly VirtualizeOptions _options = new()
{
    Overscan = 5,          // the fallback for either side
    OverscanLeading = 10,  // ahead of travel
    OverscanTrailing = 5,  // behind travel - the floor a reversal starts from
};

The leading side follows the virtualizer's scroll direction and flips when the user reverses; a list that has never scrolled is treated as scrolling forward. Biasing toward travel is the default in react-window's _getRangeToRender, react-virtualized's defaultOverscanIndicesGetter and Angular CDK's FixedSizeVirtualScrollStrategy.

⚠️ OverscanTrailing is held at all times, not only while scrolling - which is where this deliberately parts company with react-window. React re-renders when its isScrolling state flips, so its range genuinely recomputes at rest. virtual-core memoizes getVirtualIndexes on [rangeExtractor, overscan, count, startIndex, endIndex], and isScrolling is not among them, so an extractor that read it would keep returning the scrolling-time answer until the visible range moved again - the bias would outlive the scroll rather than lapse with it.

Holding the trailing side instead means a reversal is never worse than a symmetric window of the same size. Leave OverscanTrailing at whatever Overscan you would otherwise have used and treat OverscanLeading as the addition. Raise it further when unmounting an item is expensive - a row owning a <video>, say - since those items are already mounted and holding them costs far less than rebuilding them.

OnLayoutMeasured fires after each measurement pass. Use it when extra content must be appended only once the preceding row's real height is known, so the new rows land correctly on first paint instead of estimating and snapping.

It is a plain Func<Task> rather than an EventCallback on purpose: EventCallback calls StateHasChanged on the receiver after the handler returns, which on a page with a debounced search input re-renders the bound input with a stale value and clobbers in-flight keystrokes.

Coverage vs. @tanstack/virtual-core 3.17.7

Complete. Every VirtualizerOptions member is reachable, and every Virtualizer member that means something to a .NET caller is surfaced.

Axis virtual-core This package
VirtualizerOptions members 36 36

Set through Options (25): overscan, horizontal, lanes, laneAssignmentMode, gap, paddingStart, paddingEnd, scrollPaddingStart, scrollPaddingEnd, scrollMargin, initialOffset, initialRect, initialMeasurementsCache, anchorTo, followOnAppend, scrollEndThreshold, isScrollingResetDelay, useScrollendEvent, isRtl, enabled, useAnimationFrameWithResizeObserver, useCachedMeasurements, indexAttribute, debug, plus rangeExtractor reshaped as AlwaysRenderedIndexes and OverscanLeading / OverscanTrailing.

Supplied by the component (11), because virtual-core requires them as functions called on every scroll frame - marshalling them to .NET would mean an interop round-trip per frame: count (from Items), estimateSize (ItemSizeEstimator / EstimatedItemSize), getItemKey (ItemKeyExtractor), measureElement, onChange, and the scroll-source trio getScrollElement / observeElementOffset / observeElementRect / scrollToFn - which UseWindowScroller switches between the element and window variants (observeWindowOffset, observeWindowRect, windowScroll).

Methods: ScrollToIndex, ScrollToOffset, ScrollToEnd, ScrollBy, ScrollToTop, ResizeItem, Measure, GetTotalSize, GetVirtualIndexes, GetOffsetForIndex, GetVirtualItemForOffset, GetDistanceFromEnd, GetSnapshot (scroll offset, total size, isScrolling, isAtEnd, direction and the rendered range), TakeSnapshot, plus InvalidateItemSize - which has no upstream equivalent; it evicts one key from the measurement cache and bumps the cache version.

Deliberately not surfaced: virtual-core's internal plumbing - elementsCache, targetWindow, updateDeps, calculateRange, indexFromElement, shouldAdjustScrollPositionOnItemSizeChange, scrollAdjustments, setOptions (driven by the Options parameter) and the raw options / measurementsCache / itemSizeCache fields, which GetSnapshot and TakeSnapshot expose in a typed form instead.

⚠️ isAtEnd is a method upstream, not a property ((threshold?: number) => boolean). Reading it as a property yields the function object, which is never === true - so a snapshot would report "not at end" forever. Pinned by ScrollToEnd_ReachesTheEndOfTheList.

Coverage is measured against virtual-core's published dist/esm/index.d.ts - the VirtualizerOptions interface and the Virtualizer class - excluding underscore-prefixed internals.

A note on upstream internals

The interop calls three underscore-prefixed virtual-core members: _willUpdate(), _didMount(), and direct reads and writes of itemSizeCache (declared public in the .d.ts, but paired with a private itemSizeCacheVersion). They are what make measurement-cache preservation across an item-set change possible, and virtual-core exposes no public equivalent.

This is a real coupling: a virtual-core release could change them without it being a documented breaking change. It is why the package pins an exact version rather than floating, and why every version bump re-verifies those three members before shipping.

Vendored library provenance

Version @tanstack/virtual-core 3.17.7
File wwwroot/tanstack-virtual-core.js
Source jsDelivr rollup bundle of npm @tanstack/virtual-core@3.17.7 (+esm)
SHA-256 fb7a4e29d452791c08522862d74b292418f2abdedbc52f46cf347a8bae9244e1

⚠️ Deliberately the bundled build, not the npm tarball's dist/esm/index.js.

That file is not self-contained - it imports ./lazy-measurements.js and ./utils.js as siblings. Vendoring it alone gives a module that fetches fine and then fails to evaluate, and Blazor's fingerprinted import map turns the missing sibling into a confusing Failed to fetch dynamically imported module naming the outer module. The jsDelivr bundle is a single self-contained ESM file built from that same npm release, with the source stated in its own header. Its private-API surface (_willUpdate, _didMount, itemSizeCache, itemSizeCacheVersion) is re-verified on every bump.

License

MIT. @tanstack/virtual-core is itself MIT licensed.

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.17.7.2 114 8/9/2026
3.17.7 103 8/4/2026

Render-path allocation work. Row keys, placements and the stale-row filtering are now resolved once per change of the window, item set or options instead of on every render, which removed several string allocations per rendered row per render batch. The item-set snapshot crosses interop as two parallel arrays (keys, sizes) rather than an array of objects, cutting both the allocation count and the JSON payload for large sets; ItemDescriptor is gone as a result. Single-lane lists no longer format their cross-axis constants per row.

Adds OverscanLeading / OverscanTrailing: overscan biased toward the side the user is scrolling towards, as react-window, react-virtualized and Angular CDK all do by default. The leading side follows the scroll direction and flips on reversal; the trailing side is a floor held at all times rather than one that lapses when scrolling stops, because virtual-core memoizes getVirtualIndexes on [rangeExtractor, overscan, count, startIndex, endIndex] - isScrolling is not among them, so a value that lapsed would simply never be recomputed. Compiled into the same rangeExtractor as AlwaysRenderedIndexes, so the two now compose instead of one replacing the other.

Wraps @tanstack/virtual-core 3.17.7 with complete option coverage - all 36 VirtualizerOptions are reachable. AdaptiveVirtualize<TItem> measures every item exactly, caches by item key, preserves measurements across item-set changes, and exposes targeted InvalidateItemSize(key). Layouts: vertical, horizontal, multi-lane masonry (per-item Lane), gap and padding as virtualizer geometry, RTL, and a window-scroller mode. Behaviour: FollowOnAppend and AnchorTo for chat-style lists, AlwaysRenderedIndexes (upstream's rangeExtractor reshaped), and IsEnabled suspend/reset. Imperative API: ScrollToIndex, ScrollToOffset, ScrollToEnd, ScrollBy, ScrollToTop, ResizeItem, Measure, GetTotalSize, GetVirtualIndexes, GetOffsetForIndex, GetVirtualItemForOffset, GetDistanceFromEnd, GetSnapshot and TakeSnapshot. Verified by 24 real-browser Playwright tests plus unit tests.