Data Display

VirtualList

Fixed-height windowing primitive for long vertical lists that renders only the visible rows plus overscan inside a native scroll container.

import { VirtualList } from '@lostgradient/cinder/virtual-list';
listvirtualizationperformance
01

Overview

Fixed-height windowing primitive for long append-only lists. VirtualList owns a native vertical scroll container and renders only the visible rows plus overscan; you own the row markup through the row snippet.

Version 1 requires a fixed itemHeight in pixels. Variable or measured row heights are intentionally out of scope for this primitive.

Usage

svelte
<script lang="ts">
  import { VirtualList } from '@lostgradient/cinder/virtual-list';

  const events = Array.from({ length: 10_000 }, (_, index) => ({
    id: `event-${index}`,
    label: `Event ${index}`,
  }));
</script>

<VirtualList
  items={events}
  itemHeight={32}
  height="20rem"
  getKey={(event) => event.id}
  aria-label="Events"
>
  {#snippet row(event, context)}
    <div data-index={context.index}>{event.label}</div>
  {/snippet}
</VirtualList>

Use stickToBottom for live log tails: appending while the user is already at the bottom keeps the newest row in view, while appending with the viewport scrolled up leaves the scroll position unchanged.

Live preview
02

When to use

Use when
  • Rendering thousands of same-height append-only rows such as logs, event streams, or activity feeds.
  • You need a reusable primitive that owns native vertical scrolling but leaves row markup to a snippet.
Avoid when
  • Rows have substantially variable heights that must be measured dynamically — v1 requires a fixed itemHeight.
  • Rendering columns or two-dimensional grids — use data-grid for grid semantics and column virtualization.
03

Examples

10,000 item virtual list

A native scroll container rendering only the visible slice of a 10,000 item append-only event stream.

04

Props

Props for virtual-list
Name Type Default Required Bindable Description
items readonly Item[] req Items in full logical order. Only the visible window is mounted.
itemHeight number req Fixed row height in pixels. Variable and measured row heights are out of scope for v1; pass the known or estimated fixed height for every row.
overscan number 5 Extra rows rendered before and after the visible window. Defaults to 5.
height text '20rem' CSS block-size for the native scroll container. Defaults to "20rem".
stickToBottom boolean false When true, appending items while the viewport is already at the bottom keeps the newest item pinned in view. Appending while scrolled up leaves the scroll position unchanged.
tabindex number 0 Override the default focus behavior. The component sets tabindex="0" by default so keyboard users can reach the native scroll container for arrow-key scrolling. Pass tabindex={-1} when the viewport should be programmatically focusable without entering the tab order.
getKey (item: Item, index: number) => VirtualListKey Stable key extractor. Omit only when items are append-only and never reordered; the component will fall back to full-array indexes.
row snippet req Rendered row snippet. Receives the item and its virtual row context.
role unknown 'list'
onscroll unknown req