elements
Demos UI Feed Discuss Install
Style System
  • Style
  • Themes
  • Behaviors
Forms
  • Button
  • ButtonGroup
  • Checkbox
  • ColorPicker
  • Combobox
  • Field
  • FileUpload
  • Input
  • InputGroup
  • InputOTP
  • Label
  • NativeSelect
  • NumberInput
  • PasswordInput
  • RadioGroup
  • Select
  • Slider
  • Switch
  • TagsInput
  • Textarea
  • Toggle
  • ToggleGroup
Overlays
  • AlertDialog
  • Command
  • ContextMenu
  • Dialog
  • Drawer
  • DropdownMenu
  • HoverCard
  • Menubar
  • NavigationMenu
  • Popover
  • Sheet
  • Toast
  • Tooltip
Display
  • Accordion
  • Alert
  • AspectRatio
  • Avatar
  • Badge
  • Breadcrumb
  • Card
  • Collapsible
  • Empty
  • Item
  • Kbd
  • Pagination
  • Progress
  • Separator
  • Skeleton
  • Spinner
  • Table
  • Tabs
  • Tree
Layout
  • Carousel
  • Resizable
  • ScrollArea
  • Sidebar
Dates
  • Calendar
  • DatePicker
  • DateRangePicker
  • TimePicker
Data
  • AreaChart
  • BarChart
  • Chart
  • DataTable
  • Kanban
  • LineChart
  • PieChart
  • RadarChart
  • RadialChart
  • VirtualList
Chat
  • Attachment
  • Message
  • MessageList
  • PromptInput
Elements UI / Data / VirtualList

VirtualList

A long list, or a grid of rows and columns, that renders only the items near the viewport, with measured row heights.

Ten thousand rows

Only the rows near the viewport are in the DOM. range is two-way: the list writes it, the caller loops over it.

1 Ada Lovelace ada.lovelace0@example.com
2 Grace Lovelace grace.lovelace1@example.com
3 Alan Lovelace alan.lovelace2@example.com
4 Edsger Lovelace edsger.lovelace3@example.com
5 Barbara Lovelace barbara.lovelace4@example.com
6 Donald Lovelace donald.lovelace5@example.com
7 Frances Lovelace frances.lovelace6@example.com
8 Ken Lovelace ken.lovelace7@example.com
9 Margaret Lovelace margaret.lovelace8@example.com
10 Dennis Lovelace dennis.lovelace9@example.com
11 Radia Lovelace radia.lovelace10@example.com
12 Tim Lovelace tim.lovelace11@example.com
rendered: 12 · first: 1 · scrolling: false
Code
<VirtualList count={rows.length} range={state.range} estimateSize={40} label="People" class="list">
  <VirtualItem e:for={i of state.range} e:key={(i) => rows[i].id} index={i}>
    <div class="row">{rows[i].name}</div>
  </VirtualItem>
</VirtualList>

Usage

import { VirtualList, VirtualItem, VirtualCell, initialRange } from "@elements/ui/virtual-list";

<!-- private state = { range: initialRange(people.length, 20) } -->
<VirtualList count={people.length} range={state.range} estimateSize={48} label="People" class="people-list">
  <VirtualItem e:for={i of state.range} e:key={(i) => people[i].id} index={i}>
    {people[i].name}
  </VirtualItem>
</VirtualList>

<!-- the page scrolls instead of the list -->
<VirtualList scroller="window" count={rows.length} range={state.range}>...</VirtualList>

<!-- a grid: rows and columns both window in one scroller -->
<VirtualList role="grid" count={10000} columns={1000} range={state.rows} columnRange={state.cols} estimateSize={36} estimateColumnSize={120}>
  <VirtualItem e:for={r of state.rows} e:key={(r) => r} index={r}>
    <VirtualCell e:for={c of state.cols} e:key={(c) => c} column={c}>{r}, {c}</VirtualCell>
  </VirtualItem>
</VirtualList>

Examples

Rows of any height

Each row is measured as it renders; estimateSize only sizes the scrollbar until then.

Ada LovelaceThe renders scrolls the into row of a the list so into scrollbar a as any jump estimate measures a place settles measurement it height and turns every row without as the renders scrolls the into row.
Grace LovelaceList so into scrollbar a as any.
Alan LovelaceMeasures a place settles measurement it height and turns every row without as the renders scrolls the.
Edsger LovelaceEvery row without as the renders.
Barbara LovelaceRow of a the list so into scrollbar a as any jump estimate measures a place settles measurement it height and turns every row without as the renders scrolls the into row of a the list so into scrollbar.
Donald LovelaceAs any jump estimate measures a place settles.
Frances LovelaceIt height and turns every row without as the renders scrolls the into row of a the list so into scrollbar a as.
Ken LovelaceRenders scrolls the into row of a the list so into scrollbar a as any jump estimate measures a place settles measurement.
Margaret LovelaceSo into scrollbar a.
Dennis LovelaceA place settles measurement it.
Radia LovelaceRow without as the renders scrolls the into row of a the list so into scrollbar a as any jump estimate measures a place settles measurement it height and turns every row.
Tim LovelaceOf a the list so into scrollbar a as any jump estimate measures a place settles measurement it height and turns every row without as the renders scrolls the into row of a the list so into.
Code
<!-- rows of any height: each is measured as it renders -->
<VirtualList count={notes.length} range={state.range} estimateSize={72}>
  <VirtualItem e:for={i of state.range} e:key={(i) => notes[i].id} index={i}>
    <p>{notes[i].note}</p>
  </VirtualItem>
</VirtualList>

Fixed size

measure=false: every row is exactly estimateSize and nothing is measured, the fastest path for uniform rows. paddingStart and paddingEnd add space around the items.

1 Ada Lovelace
2 Grace Lovelace
3 Alan Lovelace
4 Edsger Lovelace
5 Barbara Lovelace
6 Donald Lovelace
7 Frances Lovelace
8 Ken Lovelace
9 Margaret Lovelace
10 Dennis Lovelace
11 Radia Lovelace
12 Tim Lovelace
Code
<!-- measure=false: every row is exactly estimateSize, nothing is measured -->
<VirtualList count={rows.length} range={state.range} estimateSize={36} measure={false} paddingStart={8} paddingEnd={8}>...</VirtualList>

Horizontal

orientation=horizontal scrolls sideways; sizes per index come from a function. With dir=rtl the list starts at the right.

Code
<VirtualList orientation="horizontal" count={10000} range={state.range} estimateSize={(i) => i % 3 ? 96 : 140}>
  <VirtualItem e:for={i of state.range} e:key={(i) => i} index={i}>Column {i}</VirtualItem>
</VirtualList>

Masonry lanes

lanes=3 with gap: each tile goes in the shortest lane.

Code
<!-- masonry: 3 lanes, each item goes in the shortest -->
<VirtualList count={tiles} lanes={3} gap={8} range={state.range} estimateSize={(i) => heights[i % 6]}>
  <VirtualItem e:for={i of state.range} e:key={(i) => i} index={i}>...</VirtualItem>
</VirtualList>

Rows and columns

A grid of ten thousand rows by a thousand columns: columns adds a second virtualizer on the same scroller, columnRange is two-way, and each row loops over it with VirtualCell. role=grid makes rows and gridcells with their row and column index.

Code
<!-- rows and columns both window, in one scroller -->
<VirtualList role="grid" label="Sheet" count={10000} columns={1000}
             range={state.rows} columnRange={state.cols}
             estimateSize={36} estimateColumnSize={(c) => widths[c]}>
  <VirtualItem e:for={r of state.rows} e:key={(r) => r} index={r}>
    <VirtualCell e:for={c of state.cols} e:key={(c) => c} column={c}>{r}, {c}</VirtualCell>
  </VirtualItem>
</VirtualList>

Sticky headers

sticky names the header indexes; the current one stays pinned at the top.

A
Ada Allen
Ada Allen
Ada Backus
Ada Berners-Lee
Ada Dijkstra
Ada Dijkstra
Ada Hamilton
Ada Hamilton
Ada Hopper
Ada Hopper
Ada Johnson
Code
<VirtualList count={rows.length} sticky={headerIndexes} range={state.range} estimateSize={32}>
  <VirtualItem e:for={i of state.range} e:key={(i) => rows[i].id} index={i}>
    <div class={rows[i].header ? "header" : "contact"}>{rows[i].text}</div>
  </VirtualItem>
</VirtualList>

Load more at the end

onEndReached fires once per count when the last item renders. Pairs with a windowed LiveView's more().

loaded: 50
Code
<VirtualList count={state.loaded.length + 1} range={state.range} onEndReached={() => loadMore(state)}
             role="feed" busy={state.loading}>
  <VirtualItem e:for={i of state.range} e:key={(i) => state.loaded[i]?.id ?? "loading"} index={i}>
    <div e:if={i < state.loaded.length}>{state.loaded[i].name}</div>
    <div e:else>Loading more...</div>
  </VirtualItem>
</VirtualList>

Chat: anchored to the end

anchor=end keeps the bottom still as items change; followOnAppend keeps new messages in view.

messages: 60
Code
<!-- anchor end: new messages stay in view when you are at the bottom -->
<VirtualList count={state.messages.length} anchor="end" followOnAppend initialOffset={999999} range={state.range}>...</VirtualList>

Chat: smooth follow

followOnAppend="smooth": a new message at the end glides into view instead of jumping

messages: 60
Code
<VirtualList count={state.messages.length} anchor="end" followOnAppend="smooth" initialOffset={999999} range={state.range}>...</VirtualList>

Scroll to an item

onVirtualizer hands over the virtualizer: scrollToIndex with align and smooth behavior. scrollPaddingStart keeps 8px above an item scrolled to the start.

0 Ada Lovelace
1 Grace Lovelace
2 Alan Lovelace
3 Edsger Lovelace
4 Barbara Lovelace
5 Donald Lovelace
6 Frances Lovelace
7 Ken Lovelace
8 Margaret Lovelace
9 Dennis Lovelace
10 Radia Lovelace
11 Tim Lovelace
Code
<VirtualList count={rows.length} range={state.range} onVirtualizer={(v) => list = v}>...</VirtualList>
<button type="button" onclick={() => list.scrollToIndex(5000, { align: "start" })}>Go to 5000</button>

The page scrolls

scroller=window: no inner scrollbar; the list is as tall as its items and renders the part of it on screen.

Code
<!-- the page scrolls; the list is as tall as its items -->
<VirtualList scroller="window" count={rows.length} range={state.range} estimateSize={40}>...</VirtualList>

Range extractor

rangeExtractor picks the indexes to render: here the default window plus row 1, which stays in the DOM however far the list scrolls (for a pinned row, or focus kept on it).

1 Ada Lovelace
2 Grace Lovelace
3 Alan Lovelace
4 Edsger Lovelace
5 Barbara Lovelace
6 Donald Lovelace
7 Frances Lovelace
8 Ken Lovelace
9 Margaret Lovelace
10 Dennis Lovelace
11 Radia Lovelace
12 Tim Lovelace
Code
<VirtualList count={rows.length} range={state.range}
             rangeExtractor={(r) => [...new Set([0, ...defaultRangeExtractor(r)])]}>...</VirtualList>

Restored sizes

virtualizerOptions passes any other virtualizer option through. initialMeasurementsCache restores sizes measured in an earlier visit (takeSnapshot), so the scrollbar is right before a row renders.

1 Ada Lovelace
2 Grace Lovelace
3 Alan Lovelace
4 Edsger Lovelace
5 Barbara Lovelace
6 Donald Lovelace
7 Frances Lovelace
8 Ken Lovelace
Code
let sizes = list.takeSnapshot();   // earlier
<VirtualList count={200} range={state.range} estimateSize={20}
             virtualizerOptions={{ initialMeasurementsCache: sizes }}>...</VirtualList>

Cached sizes, scrollend

useCachedMeasurements: the list keeps the sizes it was given (64px here) and skips measuring rows. useScrollendEvent: scrolling ends on the browser's scrollend event, not after isScrollingResetDelay (5 seconds here).

1 Ada Lovelace
2 Grace Lovelace
3 Alan Lovelace
4 Edsger Lovelace
5 Barbara Lovelace
6 Donald Lovelace
7 Frances Lovelace
8 Ken Lovelace
Code
<VirtualList count={200} range={state.range}
             virtualizerOptions={{ initialMeasurementsCache: sizes, useCachedMeasurements: true, useScrollendEvent: true, isScrollingResetDelay: 5000 }}>...</VirtualList>
Reference VirtualList in the manual Every part, attribute, slot, key and data attribute.
Prev RadialChartNext Attachment
Product
Manual Pricing Compare
Company
History Contact
Legal
Privacy Policy Acceptable Use Terms License Agreement
© Elements Dev Corporation 2026