Skip to content

Virtual scroll [Draft]

Radoslav Karaivanov edited this page Jun 1, 2026 · 1 revision

Virtual Scroll Specification

Owned By

Team Name: Design and Web Development

Developer name: Radoslav Karaivanov

Designer name: N/A

Requires approval from:

  • Damyan Petev
  • Radoslav Mirchev

Signed off by:

  • Damyan Petev
  • Radoslav Mirchev

Revision history

Version Author Date Description
1 Radoslav Karaivanov 2026-06-01 Initial draft

Overview

igc-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, items that leave the viewport are destroyed and new items entering the viewport are created. 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, fixed and variable item sizes, and an infinite/remote-data loading pattern through its igcDataRequest event.

Acceptance criteria

The igc-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 fixed and variable item sizes.
  • measure each rendered item and replace its estimated size with the actual measured size.
  • emit igcStateChange after each render with the current visible-window state.
  • emit igcDataRequest when the scroll position approaches the end of the loaded data, enabling infinite/remote-scroll patterns.
  • expose a scrollToIndex method for programmatic navigation.
  • operate as a Light DOM component, integrating transparently into the document or any shadow root without style leakage.
  • pass accessibility audits.

User stories

End-user stories

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.

Developer stories

As a developer I expect to be able to:

  • provide a data array of any type and a template function 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 igcStateChange to know which items are currently visible.
  • subscribe to igcDataRequest to 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 igc-virtual-scroll inside other components, including shadow roots, without style bleed-through.

Functionality

End-user experience

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: items that leave the viewport are removed from the DOM and new items entering the viewport are created. The over-scan buffer (over-scan attribute) controls how many extra items outside the visible area remain rendered to prevent blank flashes during fast scrolling.

Developer experience

Developers provide two things to the component:

  1. A data array of items.
  2. An itemTemplate function that receives a VirtualScrollItemContext<T> and returns a Lit TemplateResult.
const template = (ctx: VirtualScrollItemContext<Person>) => html`
  <div class="item">${ctx.index}: ${ctx.value.name}</div>
`;

scroll.data = people;
scroll.itemTemplate = template;

VirtualScrollItemContext<T> properties available inside the template:

Property Type Description
value T The current data item.
index number The absolute index within the full data array.
count number The total number of items in data.
isFirst boolean true when index === 0.
isLast boolean true when index === count - 1.

Each rendered item is wrapped in a <div data-vs-index="N"> 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 wrappers with a ResizeObserver. When an item's actual size differs from its current estimated size, the engine is updated and a re-render is scheduled. This makes the layout self-correcting for variable-height or variable-width content with no additional configuration.

Infinite scroll: When range.endIndex approaches within 5 items of the end of the loaded data, the igcDataRequest event is emitted exactly once per data batch. The component suppresses further emissions until the consumer responds with a new data reference.

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.

Localization

None applicable.

Keyboard navigation

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.


API

Properties

Property Attribute Reflected Type Default Description
data - No T[] [] The array of items to virtualize. Assigning a new array triggers an engine resize and resets the pending data-request flag.
orientation orientation Yes 'vertical' | 'horizontal' 'vertical' Scroll axis. 'vertical' scrolls on the block axis; 'horizontal' scrolls on the inline axis.
overScan over-scan No 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 estimated-item-size No 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 retain their measured sizes.
itemTemplate - No VirtualScrollItemTemplate<T> | null null A function (ctx: VirtualScrollItemContext<T>) => TemplateResult | nothing that renders each item. If null, the component renders nothing.

Methods

Method Signature Description
scrollToIndex (index: number, options?: ScrollToOptions): void Programmatically scrolls the container so that the item at index is at the leading edge of the viewport. Accepts the standard ScrollToOptions (e.g. { behavior: 'smooth' }).

Events

Event Type Description
igcStateChange CustomEvent<VirtualScrollState> Emitted after each render pass with a snapshot of the current visible window. Fires only when at least one item is rendered (endIndex >= startIndex).
igcDataRequest CustomEvent<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 starting at this index.
count number Suggested number of items to load (max(overScan x 4, 20)).

Slots

None. The component renders into its own Light DOM; items are created directly by the itemTemplate function without slots.

CSS Shadow Parts

None. The component does not have a Shadow Root.

CSS Variables

None. Default host styles are injected via the Constructable Stylesheet API (adoptedStyleSheets) and do not expose CSS custom properties. Consumer applications style the igc-virtual-scroll host selector and their item template content directly.


Test scenarios

Automation

  • Verify that the component renders and passes the accessibility audit (lightDom.to.be.accessible()).
  • Verify that default property values are correct (data: [], orientation: 'vertical', overScan: 2, estimatedItemSize: 50, itemTemplate: null).
  • Verify that the orientation attribute is reflected on the host element.
  • Verify that setting data and itemTemplate renders the track ([part="igc-vs-track"]) and content ([part="igc-vs-content"]) elements.
  • Verify that setting only data without an itemTemplate renders nothing.
  • Verify that the track height/width equals data.length x estimatedItemSize on initial render (before measurement).
  • Verify that updating data with a larger array resizes the track element accordingly.
  • Verify that igcStateChange is emitted after render when at least one item is visible.
  • Verify that igcDataRequest is emitted when endIndex is within 5 items of data.length.
  • Verify that igcDataRequest is not emitted again until data is updated with a new reference.
  • Verify that scrollToIndex sets scrollTop for vertical orientation.
  • Verify that scrollToIndex sets scrollLeft for horizontal orientation.

Accessibility

igc-virtual-scroll is a transparent scroll container. All semantic roles and ARIA attributes are the responsibility of the itemTemplate content provided by the developer.

The internal track and content wrapper elements are presentational and do not need to be exposed to assistive technologies; any content rendered in the Light DOM inherits the document's accessibility tree naturally.

The host element is a display: block; overflow: auto container with no intrinsic ARIA role. It does not manage focus internally.

References

RTL

Horizontal orientation works correctly in RTL contexts. The component reads scrollLeft, which browsers normalize for RTL layouts. No additional configuration is required.


Assumptions and limitations

  • The itemTemplate function is invoked once per rendered item per render cycle. Expensive template computation should be memoized by the consumer.
  • The data array is treated as an immutable snapshot. Mutating the array in place without reassigning the data property will not trigger a re-render.
  • Changing estimatedItemSize after data is loaded does not retroactively update items that have already been measured; it applies only to new items added by a subsequent data resize.
  • 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 feed role pattern on top of this component.
  • The data items are used as positional render slots; the component does not perform identity-keyed diffing (no repeat directive). Reordering items within the visible window is handled by in-place patching, not DOM node movement.

Clone this wiki locally