# Virtualize
**Also known as:** VirtualScroll, Windowing
## Description
BitVirtualize is a high-performance virtualization (windowing) component that renders only the items currently visible in its scroll viewport (plus a configurable overscan buffer). It supports fixed and dynamically measured item sizes, vertical and horizontal orientation, in-memory or lazy-loaded data, placeholders, sticky group headers, and a bottom-anchored (chat) mode.
## Parameters
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| ChildContent | `RenderFragment<TItem>?` | null | The custom template to render each item. |
| Dynamic | `bool` | false | Enables dynamic item sizing in which each rendered item gets measured in the browser and its real size gets cached, using the EstimatedItemSize for the items that have not been measured yet. |
| EmptyTemplate | `RenderFragment?` | null | The custom template to render when there is no item available. |
| EstimatedItemSize | `float` | 50 | The assumed size in pixels of the items that have not been measured yet in dynamic mode. |
| Horizontal | `bool` | false | Renders the items horizontally so the viewport scrolls along the x-axis. |
| InitialIndex | `int?` | null | The index of the item to scroll to on the first render. Ignored when Reversed is set. |
| IsStickyItem | `Func<TItem, bool>?` | null | A predicate that marks certain items (for example, group headers) as sticky. The active sticky item gets pinned to the leading edge of the viewport while its group scrolls. Fully supported with in-memory Items; in provider mode it is applied on a best-effort basis to the currently loaded window. |
| Items | `ICollection<TItem>?` | null | The in-memory collection of items to virtualize. Mutually exclusive with ItemsProvider. |
| ItemKey | `Func<TItem, object>?` | null | A function that returns a stable identity key for an item. When provided, rendered rows are keyed by identity (instead of by index) so per-item DOM/component state survives insertions, removals and reordering, and dynamic measurements follow their item across those mutations. |
| ItemSize | `float` | 50 | The size in pixels of each item along the scroll axis when the Dynamic mode is off. |
| ItemsProvider | `BitVirtualizeItemsProvider<TItem>?` | null | The item provider function that lazily supplies windows of items on demand. Mutually exclusive with Items. |
| ItemTemplate | `RenderFragment<TItem>?` | null | Alias for ChildContent. |
| LoadingTemplate | `RenderFragment?` | null | The custom template to render before the component performs its first load. |
| OnEndReached | `EventCallback` | | The callback to be called when the last item comes within ReachedThreshold items of the visible window, useful for appending more data in infinite scrolling scenarios. Fires once per item-count value. |
| OnStartReached | `EventCallback` | | The callback to be called when the first item comes within ReachedThreshold items of the visible window, useful for prepending older data (for example, loading chat history when scrolling up). |
| OnVisibleRangeChanged | `EventCallback<(int Start, int End)>` | | The callback to be called whenever the visible index range changes. |
| OverscanCount | `int` | 3 | The number of extra items to render on each side of the visible window for smoother scrolling. |
| PlaceholderTemplate | `RenderFragment<BitVirtualizePlaceholderContext>?` | null | The custom template to render an item whose data has not been loaded yet in provider mode. |
| ReachedThreshold | `int` | 0 | The number of items away from an edge the visible window must be before OnEndReached/OnStartReached fire. |
| Reversed | `bool` | false | Enables the bottom-anchored mode in which the list starts scrolled to the end and automatically keeps the newest items in view when data gets appended while the user is at the bottom. Ideal for chat and log views. |
| StickyTemplate | `RenderFragment<TItem>?` | null | The custom template to render the pinned sticky item. Falls back to the item template when not provided. |
| AriaLabel | `string?` | null | Gets or sets the accessible label for the component, used by assistive technologies. |
| Class | `string?` | null | Gets or sets the CSS class name(s) to apply to the rendered element. |
| Dir | `BitDir?` | null | Gets or sets the text directionality for the component's content. |
| HtmlAttributes | `Dictionary<string, object>` | new Dictionary<string, object>() | Captures additional HTML attributes to be applied to the rendered element, in addition to the component's parameters. |
| Id | `string?` | null | Gets or sets the unique identifier for the component's root element. |
| IsEnabled | `bool` | true | Gets or sets a value indicating whether the component is enabled and can respond to user interaction. |
| Style | `string?` | null | Gets or sets the CSS style string to apply to the rendered element. |
| TabIndex | `string?` | null | Gets or sets the tab order index for the component when navigating with the keyboard. |
| Visibility | `BitVisibility` | BitVisibility.Visible | Gets or sets the visibility state (visible, hidden, or collapsed) of the component. |
## Public Members
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| RefreshDataAsync | `Func<Task>` | | Re-requests the data from the ItemsProvider (or re-reads the Items) and refreshes the view. |
| ScrollToIndexAsync | `Func<int, BitVirtualizeScrollAlignment, bool, Task>` | | Scrolls the viewport so that the item at the provided index becomes visible. |
| ScrollToOffsetAsync | `Func<double, bool, Task>` | | Scrolls to an absolute pixel offset along the scroll axis. |
| ScrollToStartAsync | `Func<bool, Task>` | | Scrolls to the start (top/left) of the list. |
| ScrollToEndAsync | `Func<bool, Task>` | | Scrolls to the end (bottom/right) of the list. Useful for chat and log views. |
| UniqueId | `Guid` | Guid.NewGuid() | Gets the readonly unique identifier for the component's root element, assigned when the component instance is constructed. |
| RootElement | `ElementReference` | | Gets the reference to the root HTML element associated with this component. |
## Enums
### BitVirtualizeScrollAlignment Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Auto | 0 | Scroll the minimum amount required to bring the item fully into view. |
| Start | 1 | Align the item to the start (top/left) of the viewport. |
| Center | 2 | Center the item within the viewport. |
| End | 3 | Align the item to the end (bottom/right) of the viewport. |
### BitVisibility Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Visible | 0 | The content of the component is visible. |
| Hidden | 1 | The content of the component is hidden, but the space it takes on the page remains (visibility:hidden). |
| Collapsed | 2 | The component is hidden (display:none). |
### BitDir Enum
| Name | Value | Description |
| :--- | :--- | :---------- |
| Ltr | 0 | Ltr (left to right) is to be used for languages that are written from the left to the right (like English). |
| Rtl | 1 | Rtl (right to left) is to be used for languages that are written from the right to the left (like Arabic). |
| Auto | 2 | Auto lets the user agent decide. It uses a basic algorithm as it parses the characters inside the element until it finds a character with a strong directionality, then applies that directionality to the whole element. |
## Sub Classes
### BitVirtualizeItemsProviderRequest Properties
The request passed to the ItemsProvider function for a window of items.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| StartIndex | `int` | 0 | The zero-based index of the first item requested. |
| Count | `int` | 0 | The maximum number of items requested. |
| CancellationToken | `CancellationToken` | | A token that is cancelled when this request is no longer needed. |
### BitVirtualizeItemsProviderResult<TItem> Properties
The result returned from the ItemsProvider function.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Items | `IReadOnlyList<TItem>` | | The items that were loaded for the requested window. |
| TotalItemCount | `int` | 0 | The total number of items in the underlying data source. |
### BitVirtualizePlaceholderContext Properties
The context passed to the PlaceholderTemplate while real items are being loaded.
| Name | Type | Default Value | Description |
| :--- | :--- | :------------ | :---------- |
| Index | `int` | 0 | The zero-based index of the item this placeholder represents. |
| Size | `double` | 0 | The estimated size (px) reserved for the placeholder along the scroll axis. |
## Examples
\n**Basic**:
```razor
Scroll to index
#@item.ToString("N0")
Item number @item of one million
```
```csharp
private BitVirtualize
basicRef = default!;
private readonly int[] basicItems = Enumerable.Range(0, 1_000_000).ToArray();
private int basicTargetIndex;
private int basicVisibleStart;
private int basicVisibleEnd;
private async Task ScrollToBasicIndex()
{
await basicRef.ScrollToIndexAsync(basicTargetIndex, BitVirtualizeScrollAlignment.Start, smooth: true);
}
private void OnBasicRangeChanged((int Start, int End) range)
{
(basicVisibleStart, basicVisibleEnd) = range;
StateHasChanged();
}
```
\n**ItemsProvider**:
```razor
@product.Name
record #@product.Id.ToString("N0") · loaded at @product.LoadedAt
```
```csharp
private const int TotalProducts = 100_000;
private async ValueTask> LoadProducts(BitVirtualizeItemsProviderRequest request)
{
await Task.Delay(500, request.CancellationToken); // simulate a network fetch
var items = Enumerable.Range(request.StartIndex, Math.Min(request.Count, TotalProducts - request.StartIndex))
.Select(i => new Product(i, $"Product {i:N0}", DateTime.Now.ToString("HH:mm:ss")))
.ToList();
return new(items, TotalProducts);
}
public record Product(int Id, string Name, string LoadedAt);
```
\n**Dynamic size**:
```razor
```
```csharp
private List posts = [];
protected override void OnInitialized()
{
var random = new Random(42);
var words = "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua".Split(' ');
posts = Enumerable.Range(0, 10_000).Select(i =>
{
var body = string.Join(' ', Enumerable.Range(0, random.Next(5, 60)).Select(_ => words[random.Next(words.Length)]));
return new Post($"Author {i % 20}", $"{random.Next(1, 59)}m ago", body);
}).ToList();
}
public record Post(string Author, string Time, string Body);
```
\n**Horizontal**:
```razor
@item.ToString("N0")
```
```csharp
private readonly int[] horizontalItems = Enumerable.Range(0, 100_000).ToArray();
```
\n**Infinite scrolling**:
```razor
@item
@(feedLoading ? "Loading more..." : $"{feedItems.Count:N0} items loaded · scroll down to load more")
```
```csharp
private BitVirtualize feedRef = default!;
private readonly List feedItems = [.. Enumerable.Range(0, 25).Select(i => $"Feed item {i:N0}")];
private bool feedLoading;
private async Task LoadMoreFeedItems()
{
if (feedLoading) return;
feedLoading = true;
StateHasChanged();
await Task.Delay(700); // simulate a network fetch
feedItems.AddRange(Enumerable.Range(feedItems.Count, 25).Select(i => $"Feed item {i:N0}"));
feedLoading = false;
await feedRef.RefreshDataAsync();
}
```
\n**Sticky headers**:
```razor
@if (contact.IsHeader)
{
}
else
{
@contact.Name
@contact.Email
}
```
```csharp
private List contacts = [];
protected override void OnInitialized()
{
var random = new Random(11);
contacts = [];
for (var c = 'A'; c <= 'Z'; c++)
{
contacts.Add(new Contact(true, c.ToString(), string.Empty));
for (var i = 0; i < random.Next(5, 20); i++)
{
var name = $"{c}ontact {i + 1}";
contacts.Add(new Contact(false, name, $"{name.Replace(" ", ".").ToLower()}@example.com"));
}
}
}
public record Contact(bool IsHeader, string Name, string Email);
```
\n**Reversed (chat)**:
```razor
Send
```
```csharp
private BitVirtualize chatRef = default!;
private List messages = [];
private string? draftMessage;
private bool loadingChatHistory;
private int chatHistoryRemaining = 381; // count of the older messages (0..380) not loaded yet
protected override void OnInitialized()
{
// The newest messages (381..400); scrolling up loads the older ones down to 0.
messages = Enumerable.Range(381, 20).Select(i => new Message(i, i % 3 == 0, $"Message number {i}")).ToList();
}
private async Task SendChatMessage()
{
if (string.IsNullOrWhiteSpace(draftMessage)) return;
var id = messages.Count == 0 ? 1000 : Math.Max(1000, messages.Max(m => m.Id) + 1);
messages.Add(new Message(id, true, draftMessage.Trim()));
draftMessage = string.Empty;
await chatRef.RefreshDataAsync(); // Reversed mode keeps the list pinned to the bottom
}
private async Task LoadChatHistory()
{
if (loadingChatHistory || chatHistoryRemaining <= 0) return;
loadingChatHistory = true;
await Task.Delay(600); // simulate fetching older messages
var batch = Math.Min(15, chatHistoryRemaining);
messages.InsertRange(0, Enumerable.Range(chatHistoryRemaining - batch, batch).Select(i => new Message(i, i % 3 == 0, $"Message number {i}")));
chatHistoryRemaining -= batch;
loadingChatHistory = false;
await chatRef.RefreshDataAsync(); // Reversed mode preserves the scroll position
}
public record Message(int Id, bool Mine, string Text);
```