-
Notifications
You must be signed in to change notification settings - Fork 159
Virtual scroll [Draft]
- Virtual Scroll Specification
Team Name: Design and Web Development
Developer name: Radoslav Karaivanov
Designer name: N/A
- N/A
- N/A
| Version | Author | Date | Description |
|---|---|---|---|
| 1 | Radoslav Karaivanov | 2026-06-11 | Initial draft |
igx-virtual-scroll efficiently renders large lists by keeping only the items
currently visible in the viewport — plus an optional over-scan buffer — in the
DOM. As the user scrolls, embedded views that leave the viewport are detached and
pooled, and views entering the viewport are reused from the pool (or created on
demand) and patched with the new item context. A track spacer element maintains
the full virtual scroll height/width so the browser scrollbar accurately
represents the complete dataset size.
The component supports both vertical and horizontal scroll orientations
(including right-to-left layouts), fixed and variable item sizes, and an
infinite/remote-data loading pattern through its dataRequest output.
The igx-virtual-scroll must:
- render only the items currently visible in the viewport plus the over-scan buffer.
- maintain a correct scrollbar representation of the full virtual content size.
- support vertical and horizontal scroll orientations.
- support horizontal scrolling in right-to-left (RTL) layouts.
- support fixed and variable item sizes.
- measure each rendered item and replace its estimated size with the actual measured size.
- emit
stateChangeafter each render with the current visible-window state. - emit
dataRequestwhen the scroll position approaches the end of the loaded data, enabling infinite/remote-scroll patterns. - expose a
scrollToIndexmethod for programmatic navigation. - reuse embedded views via a detach/pool strategy to minimize DOM churn during steady-state scrolling.
- pass accessibility audits.
As an end-user I expect to:
- scroll through a large list smoothly, with no visible blank flickers.
- see the scroll bar accurately reflect my position within the full dataset.
- have items rendered at their actual size, not a uniform estimated size.
- scroll horizontally in right-to-left layouts with the content mirrored correctly.
As a developer I expect to be able to:
- provide a
dataarray of any type and a template to render each item. - choose between vertical and horizontal scroll orientations.
- control how many extra items are rendered beyond the visible area to tune scroll smoothness versus DOM overhead.
- provide an estimated item size to improve initial layout before actual sizes are measured.
- subscribe to
stateChangeto know which items are currently visible. - subscribe to
dataRequestto append more data when the user scrolls near the end (infinite scroll / remote paging). - programmatically scroll the component to a specific item index using
scrollToIndex. - nest
igx-virtual-scrollinside other components without style bleed-through.
The component renders as a scrollable container. Items are laid out inside an
absolutely-positioned content wrapper that is translated (via CSS transform) to
match the current scroll position. A track spacer element expands to the full
virtual size to create the correct scroll range for the browser.
When the user scrolls, the visible window shifts: views that leave the viewport
are detached and returned to a reuse pool, and views entering the viewport are
re-inserted and patched. The over-scan buffer (overScan input) controls how
many extra items outside the visible area remain rendered to prevent blank
flashes during fast scrolling.
Developers provide two things to the component:
- A
dataarray of items. - An item template, supplied either as a content
ng-templatedecorated with theigxVirtualItemdirective, or programmatically through theitemTemplateinput. When both are present,itemTemplatetakes precedence.
<igx-virtual-scroll [data]="people" style="height: 400px;">
<ng-template igxVirtualItem let-person let-i="index">
<div class="item">{{ i }}: {{ person.name }}</div>
</ng-template>
</igx-virtual-scroll>@Component({ /* ... */ })
export class MyComponent {
protected readonly people = signal(
Array.from({ length: 10_000 }, (_, i) => ({ name: `Item ${i}` })),
);
}IgxVsItemContext<T> properties available inside the template:
| Property | Type | Description |
|---|---|---|
$implicit (or let-item) |
T |
The current data item. |
index |
number |
The absolute index within the full data. |
count |
number |
The total number of items in data. |
first |
boolean |
true when index === 0. |
last |
boolean |
true when index === count - 1. |
even |
boolean |
true when index is even. |
odd |
boolean |
true when index is odd. |
Each rendered item's root element is tagged with a data-vs-index="N" attribute,
which the component uses internally to associate ResizeObserver measurements
back to the correct index in the engine.
Size measurement: After each render the component observes all rendered item
roots with a ResizeObserver. When an item's actual size differs from its
current estimated size, the engine is updated and the dependent signals
(_visibleRange, _spaceSize, _contentTransform) recompute. This makes the
layout self-correcting for variable-height or variable-width content with no
additional configuration.
Infinite scroll: When the visible endIndex approaches within 5 items of the
end of the loaded data, the dataRequest event is emitted exactly once per data
batch. A _hasPendingDataRequest flag suppresses further emissions until the
consumer responds with a new data reference. The event is also suppressed on the
initial render (before the user has scrolled).
Coordinate compression: For extremely large datasets where the virtual total size exceeds the browser's maximum scroll coordinate (~33.5 million px in Chrome), the component transparently maps virtual positions to DOM scroll positions using a virtual ratio. Developers do not need to account for this.
None applicable.
The component itself is not focusable. Keyboard scrolling (arrow keys,
Page Up/Down, Home, End) operates natively on the scrollable host element as with
any overflow: auto container. Focus management within the rendered list items is
the responsibility of the itemTemplate content.
| Input | Type | Default | Description |
|---|---|---|---|
data |
T[] |
[] |
The array of items to virtualize. Assigning a new array triggers an engine resize and resets the pending data-request flag. |
orientation |
'vertical' | 'horizontal' |
'vertical' |
Scroll axis. 'vertical' scrolls on the block axis; 'horizontal' scrolls on the inline axis. |
overScan |
number |
2 |
Number of extra items to render beyond the visible area on each side. Higher values reduce blank flashes at the cost of additional DOM nodes. |
estimatedItemSize |
number |
50 |
Estimated item size in pixels used for initial layout before items are measured in the DOM. Applies only to newly-added items; already-measured items keep sizes. |
itemTemplate |
TemplateRef<IgxVsItemContext<T>> | null |
null |
A template that renders each item, taking precedence over a content ng-template[igxVirtualItem]. If neither is provided, the component renders nothing. |
| Method | Signature | Description |
|---|---|---|
scrollToIndex |
(index: number): void |
Programmatically scrolls the container so that the item at index is at the leading edge of the viewport. Mirrored for RTL layouts. |
| Output | Type | Description |
|---|---|---|
stateChange |
EventEmitter<VirtualScrollState> |
Emitted after each render pass with a snapshot of the current visible window. Fires only when at least one item is rendered. |
dataRequest |
EventEmitter<VirtualScrollDataRequest> |
Emitted once when the visible range comes within 5 items of data.length. Suppressed until a new data reference is assigned. |
VirtualScrollState
| Property | Type | Description |
|---|---|---|
startIndex |
number |
Index of the first rendered item (inclusive). |
endIndex |
number |
Index of the last rendered item (inclusive). |
viewportSize |
number |
Current size of the scrollable viewport in px. |
totalSize |
number |
Total virtual size of all items in px. |
VirtualScrollDataRequest
| Property | Type | Description |
|---|---|---|
startIndex |
number |
The first index for which data is not yet available. Append at least count items from here. |
count |
number |
Suggested number of items to load (max(overScan × 4, 20)). |
Items are projected through a single item template. The template is resolved in the following order of precedence:
- The
itemTemplateinput, when set. - A content
ng-templatedecorated with theigxVirtualItem(IgxVirtualItemDirective) directive.
If neither is provided, the component renders nothing.
| Class | Applied to | Description |
|---|---|---|
igx-virtual-scroll |
host | Base host class. |
igx-virtual-scroll--vertical |
host | Present when orientation is vertical. |
igx-virtual-scroll--horizontal |
host | Present when orientation is horizontal. |
igx-vs__track |
element | Spacer element sized to the full virtual size. |
igx-vs__content |
element | Absolutely-positioned, translated content wrapper. |
None. The component ships structural styles only and does not expose CSS custom
properties. Consumer applications style the igx-virtual-scroll host selector and
their item template content directly.
- Verify that the component renders and exposes
role="list"and theigx-virtual-scrollclass on the host. - Verify default input values (
data: [],orientation: 'vertical',overScan: 2,estimatedItemSize: 50,itemTemplate: null). - Verify that the vertical/horizontal modifier class is applied based on
orientation. - Verify that setting
dataand providing a template renders the track (.igx-vs__track) and content (.igx-vs__content) elements. - Verify that the component renders only a subset of items (not the whole dataset).
- Verify that providing only
datawithout any template renders nothing. - Verify that the track height/width reflects the virtual size on initial render.
- Verify that updating
datawith a larger array resizes the track element. - Verify that
stateChangeis emitted after render when at least one item is visible, withstartIndex,endIndex,viewportSize, andtotalSize. - Verify that
dataRequestis emitted whenendIndexis within 5 items ofdata.lengthand the user has scrolled. - Verify that
dataRequestis not emitted on the initial render. - Verify that
dataRequestis not emitted again untildatais updated with a new reference. - Verify that
scrollToIndexsetsscrollTopfor vertical orientation. - Verify that
scrollToIndexsetsscrollLeftfor horizontal orientation. - Verify that
scrollToIndexsets a negativescrollLeftfor RTL horizontal orientation. - Verify that the content wrapper is translated in the negative direction for RTL horizontal orientation.
- Verify that
IgxVirtualItemDirectiveis picked up as a content child and exposes a non-nullTemplateRef.
igx-virtual-scroll is primarily a scroll container. The host element carries
role="list"; the semantics of each rendered item are the responsibility of the
itemTemplate content provided by the developer.
The internal track and content wrapper elements are presentational
(aria-hidden="true" on the track) and are not exposed to assistive technologies;
any content rendered into the item template inherits the document's accessibility
tree naturally.
The host is a display: block; overflow: auto container. It does not manage focus
internally.
-
ARIA:
feedrole — a recommended pattern for infinite-scroll containers requiring keyboard navigation. - ResizeObserver API
- CSS
containproperty
Horizontal orientation works correctly in right-to-left contexts. The component
detects the host's resolved direction via the isLeftToRight utility and:
- normalizes the browser's negative
scrollLeftin RTL into a positive virtual scroll position, keeping all engine math direction-agnostic; - applies the content wrapper's
translateXoffset in the negative (leading) direction; - negates the
scrollLefttarget inscrollToIndex.
The content wrapper is additionally re-anchored to the right edge of the track via
the :host(.igx-virtual-scroll--horizontal:dir(rtl)) style rule. No additional
configuration is required from the consumer.
- The item template is invoked once per rendered item per render cycle. Expensive template computation should be memoized by the consumer.
- The
dataarray is treated as an immutable snapshot. Mutating the array in place without reassigning thedatainput will not trigger a re-render. - Changing
estimatedItemSizeafter data is loaded does not retroactively update items that have already been measured; it applies only to new items added by a subsequentdataresize. - Items are virtualized in a single flat sequence. Multi-column grid layouts are not natively supported; consumers must manage column layout within the item template.
- The component does not implement programmatic focus management within the
virtual list. Consumers requiring a fully keyboard-navigable infinite list
should implement the ARIA
feedrole pattern on top of this component. - The
dataitems are used as positional render slots; the component does not perform identity-keyed diffing. Reordering items within the visible window is handled by in-place context patching, not DOM node movement.