Skip to content
# Virtualize **Also known as:** VirtualScroll, Windowing, VirtualGrid ## Description BitVirtualize renders only the items in its scroll viewport, plus a small overscan buffer, so lists of millions of items scroll as smoothly as a few dozen. Items can share one size or be measured as they render, come from memory or be loaded window by window, scroll vertically or horizontally (by themselves, or with an ancestor or the page) and be laid out in a fixed or responsive grid of lanes. It adds header and footer content, sticky group headers, infinite scrolling in both directions, a bottom-anchored chat mode, programmatic scrolling and full keyboard and screen-reader support. ## Parameters | Name | Type | Default Value | Description | | :--- | :--- | :------------ | :---------- | | AlignToEnd | `bool` | false | Pushes the items to the end (bottom, or the right in horizontal mode) of the viewport while they are too few to fill it, the way a chat conversation starts at the bottom. Pairs naturally with Reversed. | | ChildContent | `RenderFragment<TItem>?` | null | The custom template to render each item. | | Classes | `BitVirtualizeClassStyles?` | null | Custom CSS classes for different parts of the BitVirtualize. | | 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. | | Gap | `float` | 0 | The space in pixels between consecutive items along the scroll axis, and between the lanes of a grid. | | FooterTemplate | `RenderFragment?` | null | The custom template to render after the last item, inside the scroll container (for example, a loading indicator at the end of an infinite list). | | HeaderTemplate | `RenderFragment?` | null | The custom template to render before the first item, inside the scroll container. | | Horizontal | `bool` | false | Renders the items horizontally so the viewport scrolls along the x-axis. | | IndexedItemTemplate | `RenderFragment<BitVirtualizeItemContext<TItem>>?` | null | The custom template to render each item, which also receives the index of the item in the whole list (for example, for numbering or striping the rows). Takes precedence over ItemTemplate and ChildContent. | | 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. A change in the state the predicate reads, rather than in the predicate itself, gets applied by RefreshDataAsync. | | Items | `ICollection<TItem>?` | null | The in-memory collection of items to virtualize. Mutually exclusive with ItemsProvider. | | ItemAttributes | `Func<TItem, IReadOnlyDictionary<string, object>?>?` | null | A function that returns extra HTML attributes for the element of an item, which is the one that takes the keyboard focus: an aria-selected, aria-labelledby or aria-describedby for assistive technologies, a class or a style for the whole slot of the item. A role or an aria attribute it returns overrides the default one, while a class or a style is appended to the ones of the component. | | ItemKey | `Func<TItem, object>?` | null | A function that returns a stable and unique 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, dynamic measurements follow their item across those mutations, and the item in view stays in place when items get inserted or removed before it. | | ItemRole | `string?` | listitem | The ARIA role of each item element. | | ItemSize | `float` | 50 | The size in pixels of each item (each row of items with Lanes) 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. | | Lanes | `int` | 1 | The number of items laid side by side across the scroll axis, which turns the list into a virtualized grid: the items fill each row (each column in horizontal mode) from the start, every one as wide as an equal share of the row, and the arrow keys move across the lanes as well as along them. In dynamic mode a row is as tall as its tallest item. | | LoadingTemplate | `RenderFragment?` | null | The custom template to render until the component has performed its first load. | | MinLaneSize | `float?` | null | The smallest size in pixels of a lane across the scroll axis, which makes the grid responsive: the list has as many lanes as fit in its width (its height in horizontal mode), at least one. Takes precedence over Lanes once the list has been measured. | | OnAtEndChanged | `EventCallback<bool>` | | The callback to be called when the viewport arrives at the end of the list (true) or leaves it (false), for example to show a "Jump to latest" button only while the newest items are out of view. Also called with the initial state once the list has taken its initial position. The end is the one ScrollToEndAsync goes to, the FooterTemplate included; an emptied list is at both edges. | | OnAtStartChanged | `EventCallback<bool>` | | The callback to be called when the viewport arrives at the start of the list (true) or leaves it (false), for example to show a "Back to top" button only once the user has scrolled away. Also called with the initial state once the list has taken its initial position. The start is the one ScrollToStartAsync goes to, the HeaderTemplate included; an emptied list is at both edges. | | 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). Fires again when items get prepended while the start is still within reach. | | 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, the first window too: before the provider tells the count of the items (and so in a prerendered or statically rendered page) the window it is first asked for is rendered as placeholders, unless a LoadingTemplate is provided. | | 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. | | Role | `string?` | list | The ARIA role of the root element, which is a group instead while the loading or the empty content takes the place of the items. | | ScrollerSelector | `string?` | null | The CSS selector of an ancestor that scrolls the list instead of the list itself, which then grows to the full size of its items and only virtualizes them; the "window", "document", "body" and "html" values select the page itself. The list then has no height of its own to give. | | StickyTemplate | `RenderFragment<TItem>?` | null | The custom template to render the pinned sticky item. Falls back to the item template when not provided. | | Styles | `BitVirtualizeClassStyles?` | null | Custom CSS styles for different parts of the BitVirtualize. | | 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. | | Disabled | `bool` | false | Gets or sets a value indicating whether the component is disabled and cannot respond to user interaction. | | ForceAnimation | `bool` | false | Gets or sets a value indicating whether the component's animations play at their full duration even when reduced motion is requested. | | 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. | | 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, BitScrollAlignment, bool, Task>` | | Scrolls the viewport so that the item at the provided index becomes visible. A call made before the component is ready (for example, before its data arrives) gets applied once it is. | | ScrollToOffsetAsync | `Func<double, bool, Task>` | | Scrolls to an absolute pixel offset along the scroll axis, measured from the start of the first item. | | ScrollByAsync | `Func<double, bool, Task>` | | Scrolls the viewport by the provided number of pixels along the scroll axis (negative values scroll back). | | ScrollToStartAsync | `Func<bool, Task>` | | Scrolls to the very start (top/left) of the list, including the HeaderTemplate. | | ScrollToEndAsync | `Func<bool, Task>` | | Scrolls to the very end (bottom/right) of the list, including the FooterTemplate. 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 ### BitScrollAlignment Enum | Name | Value | Description | | :--- | :--- | :---------- | | Start | 0 | The element is brought to the start of the pane: its top edge to the top of the pane, and its leading edge to the leading edge of the pane. | | Center | 1 | The element is centered in the pane along both axes. | | End | 2 | The element is brought to the end of the pane: its bottom edge to the bottom of the pane, and its trailing edge to the trailing edge of the pane. | | Nearest | 3 | The pane moves as little as it can: an element that is already fully in view is not moved to at all, and one that is not is brought to whichever edge it is nearest. | ### 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. | ### BitVirtualizeItemContext<TItem> Properties The context passed to the IndexedItemTemplate: an item along with its position in the whole list. | Name | Type | Default Value | Description | | :--- | :--- | :------------ | :---------- | | Item | `TItem` | | The item to render. | | Index | `int` | 0 | The zero-based index of the item in the whole list (not in the rendered window). | ### 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. | ### BitVirtualizeClassStyles Properties Custom CSS classes/styles for the different parts of the BitVirtualize. | Name | Type | Default Value | Description | | :--- | :--- | :------------ | :---------- | | Root | `string?` | null | Custom CSS classes/styles for the root (scroll container) element of the BitVirtualize. | | Header | `string?` | null | Custom CSS classes/styles for the header container of the BitVirtualize. | | Item | `string?` | null | Custom CSS classes/styles for the wrapper element of each rendered item (and placeholder) of the BitVirtualize. | | Sticky | `string?` | null | Custom CSS classes/styles for the pinned sticky item container of the BitVirtualize. | | Footer | `string?` | null | Custom CSS classes/styles for the footer container of the BitVirtualize. | | Loading | `string?` | null | Custom CSS classes/styles for the loading container of the BitVirtualize. | | Empty | `string?` | null | Custom CSS classes/styles for the empty container of the BitVirtualize. | ## CSS Variables Public CSS custom properties read off the component's root. Set one on `:root` to restyle every instance, on any ancestor to restyle the ones inside it, or on the `Style` of one instance. | Name | Default Value | Description | | :--- | :------------ | :---------- | | `--bit-Virtualize-focus-color` | var(--bit-clr-pri-focus) | Focus indicator of the list and of its items. | | `--bit-Virtualize-sticky-background` | var(--bit-clr-bg-pri) | Background of the pinned sticky item, which hides the items scrolling under it. | | `--bit-Virtualize-sticky-shadow` | none | Shadow under the pinned sticky item. | | `--bit-Virtualize-state-color` | var(--bit-clr-fg-sec) | Text color of the loading and empty content. | | `--bit-Virtualize-state-font-size` | var(--bit-tpg-fs-sm) | Font size of the loading and empty content. | | `--bit-Virtualize-state-padding` | spacing(2) | Padding of the loading and empty content. | ## Examples \n**Basic**: ```razor
#@item.ToString("N0") Item number @item of one million
``` ```csharp private readonly int[] basicItems = Enumerable.Range(0, 1_000_000).ToArray(); ``` \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}", DateTimeOffset.Now.ToString("HH:mm:ss"))) .ToList(); return new(items, TotalProducts); } public record Product(int Id, string Name, string LoadedAt); ``` \n**Dynamic size**: ```razor
@post.Author · @post.Time
@post.Body
``` ```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**Grid (Lanes)**: ```razor
@item.ToString("N0")
``` ```csharp private readonly int[] gridItems = Enumerable.Range(0, 100_000).ToArray(); private int gridLanes = 4; private bool gridResponsive; ``` \n**Scrolling**: ```razor
Start Center End Auto
To start -500px +500px To end
#@item.ToString("N0")
``` ```csharp private BitVirtualize scrollRef = default!; private readonly int[] scrollItems = Enumerable.Range(0, 100_000).ToArray(); private int scrollTargetIndex = 5_000; private bool scrollSmooth = true; private (int Start, int End) visibleRange; private bool scrollAtStart; private bool scrollAtEnd; private async Task ScrollToTarget(BitScrollAlignment alignment) { await scrollRef.ScrollToIndexAsync(scrollTargetIndex, alignment, scrollSmooth); } ``` \n**ScrollerSelector**: ```razor
Content above the list scrolls away with it.
Item @item
Content below the list.
``` ```csharp private readonly int[] pageItems = Enumerable.Range(0, 10_000).ToArray(); ``` \n**Keyboard & accessibility**: ```razor
@article.Title @article.Summary
``` ```csharp private readonly Article[] articles = Enumerable.Range(1, 10_000) .Select(i => new Article(i, $"Headline number {i:N0}", $"A short summary of the story number {i:N0}.")) .ToArray(); private static Dictionary ArticleAttributes(Article article) => new() { ["aria-labelledby"] = $"article-title-{article.Id}", ["aria-describedby"] = $"article-summary-{article.Id}", }; public record Article(int Id, string Title, string Summary); ``` \n**Templates**: ```razor
Empty the list Refill the list
Header · @templateItems.Count.ToString("N0") items
@(context.Index + 1). Item @context.Item
No items to show
``` ```csharp private List templateItems = [.. Enumerable.Range(0, 1_000)]; ``` \n**Infinite scrolling**: ```razor
@item
``` ```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) {
@contact.Name
} else {
@contact.Name @contact.Email
}
@contact.Name
``` ```csharp private List contacts = []; protected override void OnInitialized() { var random = new Random(11); var firstNames = "Alice Bruno Chloe Daniel Emma Felix Grace Hugo Isla Jack Kira Liam Maya Noah Olivia Paul Quinn Ruby Sam Tara Umar Vera Will Xena Yusuf Zoe".Split(' '); var lastNames = "Adams Baker Clark Davis Evans Foster Green Hall Irwin Jones King Lewis Moore Nash Owen Price Reed Scott Turner".Split(' '); contacts = []; for (var c = 'A'; c <= 'Z'; c++) { contacts.Add(new Contact(true, c.ToString(), string.Empty)); var count = random.Next(5, lastNames.Length + 1); for (var i = 0; i < count; i++) { var name = $"{firstNames[c - 'A']} {lastNames[i]}"; contacts.Add(new Contact(false, name, $"{name.Replace(' ', '.').ToLower()}@example.com")); } } } public record Contact(bool IsHeader, string Name, string Email); ``` \n**ItemKey**: ```razor
Add 5 at the top Remove the first 5
``` ```csharp private List tasks = [.. Enumerable.Range(1, 1_000).Select(i => new TaskItem(i, $"Task {i}"))]; private int nextTaskId = 1_001; private void AddTasks() { tasks = [.. Enumerable.Range(0, 5).Select(_ => new TaskItem(nextTaskId, $"New task {nextTaskId++}")), .. tasks]; } private void RemoveTasks() { tasks = [.. tasks.Skip(5)]; } public record TaskItem(int Id, string Title); ``` \n**Reversed (chat)**: ```razor
@(loadingChatHistory ? "Loading older messages..." : chatHistoryRemaining > 0 ? "Scroll up for older messages" : "This is the beginning of the conversation")
@message.Text
Send
Jump to latest New conversation
``` ```csharp private BitVirtualize chatRef = default!; private List messages = []; private string? draftMessage; private bool loadingChatHistory; private bool chatAtEnd = true; 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(); // the message in view stays where it was } private void NewConversation() { chatHistoryRemaining = 0; messages = [new Message(1000, false, "Hi! How can I help you?")]; } public record Message(int Id, bool Mine, string Text); ``` \n**Cascading parameters**: ```razor
Item @item
Item @item
``` ```csharp private readonly int[] styleItems = Enumerable.Range(0, 1_000).ToArray(); private readonly BitVirtualizeParams[] virtualizeParams = [ new() { ItemSize = 40, OverscanCount = 5, Classes = new() { Root = "cascaded-list", Item = "cascaded-item" }, } ]; ``` \n**Style & Class**: ```razor
Item @item
Header
@(item % 10 == 0 ? $"Group {item / 10}" : $"Item {item}")
Item @item
Footer
@(item % 10 == 0 ? $"Group {item / 10}" : $"Item {item}")
``` ```csharp private readonly int[] styleItems = Enumerable.Range(0, 1_000).ToArray(); private const string cssVariablesStyle = "--bit-Virtualize-focus-color:crimson;" + "--bit-Virtualize-sticky-background:var(--bit-clr-bg-sec);" + "--bit-Virtualize-sticky-shadow:0 2px 6px rgba(0, 0, 0, 0.2)"; ``` \n**RTL**: ```razor
مورد @item.ToString("N0")
``` ```csharp private readonly int[] horizontalItems = Enumerable.Range(0, 100_000).ToArray(); ```