Virtualized list

SvListBox scaling to 50,000 options via fixed-row windowing (only visible rows in the DOM) - scroll + type-ahead stay instant. Plus a custom itemTemplate row (avatar + role). Just add virtual.

A live, editable Svelte 5 component example from the SvGrid gallery (Selection). Read the SvGrid UI component docs for the full API.

About this example

SvListBox and SvDropDownList scaling to 50,000 options in Svelte 5 with the virtual prop: only the visible rows exist in the DOM, rows are recycled on scroll with no mount and unmount churn, grouped lists window too with their headings, and rowHeight can be a function for variable heights. Scrolling and type-ahead stay instant, and a custom itemTemplate renders an avatar and role per row.

SvListBox / SvDropDownList virtualization - huge option sets rendered smoothly by windowing (only the visible rows exist in the DOM), now over the shared virtualizer: rows are RECYCLED on scroll (no mount/unmount churn), GROUPED lists window too (headings included), and rowHeight can be a function for variable-height rows. The public API is unchanged; add virtual.

Imports, features and API used

Imports: @svgrid/grid

Frequently asked questions

What changes when I add virtual?

Nothing in the API. The list renders only the rows in view plus a small buffer and recycles them as you scroll, so a list of fifty thousand options costs the same as one of fifty.

Do grouped lists virtualize?

Yes. Group headings are part of the windowed rows, so they scroll into view like options and type-ahead still jumps across groups.

Can rows have different heights?

Yes. Pass rowHeight as a function of the option and the virtualizer uses the per-row size.

Related documentation

Related articles

Source code (323-list-virtualization.svelte)

<script lang="ts">
  /**
   * SvListBox / SvDropDownList virtualization - huge option sets rendered
   * smoothly by windowing (only the visible rows exist in the DOM), now over the
   * shared virtualizer: rows are RECYCLED on scroll (no mount/unmount churn),
   * GROUPED lists window too (headings included), and `rowHeight` can be a
   * function for variable-height rows. The public API is unchanged; add
   * `virtual`.
   */
  import { SvListBox, SvDropDownList } from '@svgrid/grid'
  import type { ListOption } from '@svgrid/grid'

  const N = 50000
  const options: ListOption[] = Array.from({ length: N }, (_, i) => ({
    value: i,
    label: `Item ${(i + 1).toLocaleString()}`,
  }))

  // A grouped dataset: 8,000 people bucketed by role. Grouped lists are now
  // windowed too - previously enabling `virtual` fell back to rendering every
  // node, so a list this size mounted 8,000 rows.
  const roles = ['Engineering', 'Design', 'Product', 'Analytics', 'Support']
  const people: ListOption[] = Array.from({ length: 8000 }, (_, i) => ({
    value: `u${i}`,
    label: `User ${i + 1}`,
    group: roles[i % roles.length],
  }))
  const initials = (v: string | number) => `U${(Number(String(v).replace('u', '')) % 99) + 1}`

  // Variable-height rows: `rowHeight` as a function. Every 4th row is taller.
  const mixed: ListOption[] = Array.from({ length: 3000 }, (_, i) => ({
    value: i,
    label: i % 4 === 0 ? `Row ${i + 1}  -  tall (56px)` : `Row ${i + 1}  -  compact (30px)`,
  }))
  const mixedHeight = (_opt: ListOption, i: number) => (i % 4 === 0 ? 56 : 30)

  let picked = $state<string | number | null>(42)
  let member = $state<string | number | null>('u3')
  let tall = $state<string | number | null>(0)
  let ddlValue = $state<string | number | null>(1234)
</script>

<div class="wrap">
  <header>
    <h2>Virtualized list</h2>
    <p>Selects backed by up to <strong>{N.toLocaleString()}</strong> options. Only the visible rows are in the DOM and they are <strong>recycled</strong> as you scroll - grouped lists and variable-height rows window too. Just add <code>virtual</code>.</p>
  </header>

  <div class="cols">
    <div class="cell">
      <span class="ttl">{N.toLocaleString()} items ยท recycled</span>
      <SvListBox {options} value={picked} rows={9} virtual onChange={(v) => (picked = v)} />
      <span class="val">Selected: {picked}</span>
    </div>

    <div class="cell">
      <span class="ttl">Grouped + templated</span>
      <SvListBox options={people} value={member} rows={9} virtual rowHeight={44} groupHeaderHeight={26} onChange={(v) => (member = v)}>
        {#snippet itemTemplate(opt)}
          <span class="av">{initials(opt.value)}</span>
          <span class="who"><strong>{opt.label}</strong><em>{opt.group}</em></span>
        {/snippet}
      </SvListBox>
      <span class="val">8,000 people, windowed by group</span>
    </div>

    <div class="cell">
      <span class="ttl">Variable row heights</span>
      <SvListBox options={mixed} value={tall} rows={7} virtual rowHeight={mixedHeight} onChange={(v) => (tall = v)} />
      <span class="val"><code>rowHeight</code> is a function</span>
    </div>

    <div class="cell">
      <span class="ttl">Virtualized dropdown</span>
      <SvDropDownList {options} value={ddlValue} virtual onChange={(v) => (ddlValue = v)} />
      <span class="val">The dropdown panel windows the {N.toLocaleString()} options too.</span>
    </div>
  </div>
</div>

<style>
  .wrap { padding: 20px; max-width: 900px; display: flex; flex-direction: column; gap: 18px; }
  header h2 { margin: 0 0 4px; font-size: 20px; font-weight: 700; }
  header p { margin: 0; color: var(--sg-muted, #64748b); font-size: 13.5px; line-height: 1.5; max-width: 70ch; }
  code { background: var(--sg-row-hover-bg, #eef2ff); padding: 1px 5px; border-radius: 5px; font-size: 12px; }
  .cols { display: flex; gap: 28px; flex-wrap: wrap; align-items: start; }
  .cell { display: flex; flex-direction: column; gap: 8px; }
  .ttl { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; color: var(--sg-muted, #64748b); }
  .val { font-size: 12px; color: var(--sg-muted, #94a3b8); }
  .av { width: 28px; height: 28px; flex: none; border-radius: 50%; display: grid; place-items: center; font-size: 11px; font-weight: 700; background: color-mix(in srgb, var(--sg-accent, #2563eb) 16%, transparent); color: var(--sg-accent, #2563eb); }
  .who { display: flex; flex-direction: column; line-height: 1.25; overflow: hidden; }
  .who strong { font-size: 13px; }
  .who em { font-size: 11px; font-style: normal; color: var(--sg-muted, #94a3b8); }
</style>

View this example on GitHub

More Selection examples

  • Selection controls - List and overlay pickers: SvListBox (inline multi-select), SvDropDownList, SvComboBox (type to filter), SvAutoComplete (free text + suggestions), SvTagsInput (chips) and SvCountryInput (searchable, flags). Every popover portals out of the scroll container. Grid cell editors, standalone too.
  • List box - SvListBox: an assignee picker - inline multi-select with grouped options (departments), roving keyboard + type-ahead and a live summary.
  • Combo box - SvComboBox: a form field - type to filter a grouped, portalled list; the value must come from the list. With label + required validation.
  • Drop-down list - SvDropDownList: a status / priority toolbar with grouped options, type-ahead and animated portalled panels driving a live issue list.
  • Autocomplete - SvAutoComplete: a free-text search field with live suggestion shortcuts (any value accepted) - city search and a filter-syntax helper.