VirtualList
elements man ui/templates/virtual-listA long list, or a grid of rows and columns, that renders only the items near the viewport, with measured row heights.
Exampleimport { 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>
Parts
| Part | What it is |
|---|---|
VirtualList |
The scroll container. Owns the virtualizer and writes range, the indexes to render. |
VirtualItem |
One rendered item. Takes its index; the list measures it and places it at its offset. |
VirtualCell |
One cell of a grid row (columns > 0). Takes its column; the list places it at the column's offset and width. |
.virtual-list-inner |
The track inside the scroller, as long as every item together. |
Attributes
VirtualList
| Attribute | Type | Default | Description |
|---|---|---|---|
count |
number |
(required) | How many items the list has. Required. |
range |
number[] (two-way) |
[] |
The indexes to render, written by the list as it scrolls. Loop over it with <VirtualItem index>. Seed it with initialRange(count, n) so the server renders the first n items. |
estimateSize |
number | ((index: number) => number) |
40 |
Item size in px before it is measured. Sizes the scrollbar until items render. |
overscan |
number |
5 |
Extra items rendered before and after the viewport. With lanes, rows of items. |
orientation |
"vertical" | "horizontal" |
"vertical" |
Scroll axis. |
gap |
number |
0 |
Space between items in px. |
lanes |
number |
1 |
Columns (vertical) or rows (horizontal): a masonry layout; each item goes in the shortest lane. |
paddingStart |
number |
0 |
Space before the first item in px. |
paddingEnd |
number |
0 |
Space after the last item in px. |
scrollPaddingStart |
number |
0 |
Offset kept clear when scrolling an item into view at the start (a sticky header's height). |
scrollPaddingEnd |
number |
0 |
Offset kept clear at the end. |
initialOffset |
number |
0 |
Scroll position in px when the list starts. |
scroller |
"self" | "window" |
"self" |
self: the list scrolls (give it a height). window: the page scrolls and the list is as tall as its items. |
anchor |
"start" | "end" |
"start" |
The side that holds still when items change. end suits chat and logs. |
followOnAppend |
boolean | "auto" | "smooth" |
false |
With anchor end: stay at the end when items are added while already there; "smooth" glides to the new item. |
measure |
boolean |
true |
Measure each item's real size. false uses estimateSize as the exact size (fixed rows, no measuring). |
rangeExtractor |
((range) => number[]) | null |
null |
Which indexes render for the visible range. defaultRangeExtractor is exported to build on. |
sticky |
number[] |
[] |
Indexes of header items. The last one above the viewport stays rendered and pinned to the top. |
itemKey |
(index: number) => string | number |
(i) => i |
A stable key per index, so measurements follow an item when the list reorders. |
endThreshold |
number |
0 |
How many items before the end onEndReached fires. |
scrolling |
boolean (two-way) |
false |
True while the list is scrolling. Written by the list. |
busy |
boolean |
false |
With role feed, marks the feed aria-busy. Set it while the next page loads (not while scrolling). |
role |
"list" | "feed" | "grid" | "none" |
"list" |
Role of the track; items take the matching role (listitem, article, row). With grid, cells are gridcells and rows and cells carry aria-rowindex and aria-colindex. |
label |
string |
"" |
Accessible name of the list. |
dir |
"" | "ltr" | "rtl" |
"" |
Direction. A horizontal list scrolls from the right under rtl. |
class |
string |
"" |
Extra classes on the scroll container; set its height here. |
attrs |
Record<string, string | number | boolean | undefined> |
{} |
Other attributes for the scroll container: id, data-*, aria-*. |
onRangeChange |
(range: number[]) => void |
() => {} |
Called when the rendered indexes change. |
onScrollingChange |
(scrolling: boolean) => void |
() => {} |
Called when scrolling starts and stops. |
onEndReached |
() => void |
() => {} |
Called once per count when the last item renders: load the next page here. |
onVirtualizer |
(v: Virtualizer) => void |
() => {} |
Hands over the virtualizer: scrollToIndex, scrollToOffset, scrollBy, scrollToEnd, measure, getTotalSize. |
columns |
number |
0 |
Column count of a grid. Above 0, a second virtualizer on the same scroller windows the columns, and each row loops over columnRange with <VirtualCell column>. |
columnRange |
number[] (two-way) |
[] |
The column indexes to render, written by the list as it scrolls sideways. |
estimateColumnSize |
number | ((index: number) => number) |
120 |
Width of each column in px. Rows of a grid are estimateSize tall. |
columnOverscan |
number |
2 |
Extra columns rendered on each side of the viewport. |
onColumnRangeChange |
(range: number[]) => void |
() => {} |
Called when the rendered columns change. |
onColumnVirtualizer |
(v: Virtualizer) => void |
() => {} |
Hands over the column virtualizer: scrollToIndex on a column. |
virtualizerOptions |
Record<string, unknown> |
{} |
Any other virtualizer option, passed through: enabled, isScrollingResetDelay, useScrollendEvent, scrollEndThreshold, laneAssignmentMode, initialMeasurementsCache, useCachedMeasurements, initialRect, smoothScrollDuration. |
VirtualCell
| Attribute | Type | Default | Description |
|---|---|---|---|
column |
number |
(required) | The cell's column index. Required. |
class, attrs |
string, Record |
"", {} |
Extra classes and attributes on the cell. |
VirtualItem
| Attribute | Type | Default | Description |
|---|---|---|---|
index |
number |
(required) | The item's index in the list. Required. |
setsize |
number |
-1 |
aria-setsize, when the list's count is not the set size (the list sets count otherwise). |
class, attrs |
string, Record |
"", {} |
Extra classes and attributes on the item. |
Slots
| Slot | Content |
|---|---|
default |
The rendered items: <VirtualItem e:for={i of range} index={i}>. |
Keyboard
| Keys | Action |
|---|---|
Tab |
Focuses the scroll container (or the first focusable item). |
ArrowDown / ArrowUp |
Scroll the focused list (native scrolling). |
PageDown / PageUp, Home / End |
Scroll by a page, or to the start and end. |
Arrows (role grid) |
A grid with columns moves focus one cell; a cell outside the window scrolls in (one Tab stop, APG grid). |
Home / End, Ctrl + Home / Ctrl + End (role grid) |
First or last cell of the row; first cell of the grid, last cell of the last row. |
PageDown / PageUp (role grid) |
Move focus a viewport of rows. |
PageDown / PageUp (role feed) |
From inside an article, focus the next or previous article (APG feed). |
Ctrl + End / Ctrl + Home (role feed) |
Focus the first focusable element after the feed, or the last one before it. |
Data attributes
| Attribute | Values | On | Description |
|---|---|---|---|
data-orientation |
"vertical" | "horizontal" |
.virtual-list | |
data-scroller |
"self" | "window" |
.virtual-list | |
data-ready |
present | .virtual-list | The virtualizer is running; items are placed by transform. |
data-scrolling |
present | .virtual-list | While scrolling. |
data-index |
the index | .virtual-item | |
data-lane |
the lane number | .virtual-item | |
data-columns |
the column count | .virtual-list | A grid. |
data-column |
the column index | .virtual-cell | |
data-sticky |
present | .virtual-item | The pinned header item. |
Accessibility
- The track has role list (or feed, grid) and an aria-label from label; items take the matching item role.
- A grid track has aria-rowcount and aria-colcount; rows have aria-rowindex and cells are gridcells with aria-colindex, so the position in the whole grid is announced.
- There is no listbox role: a list of options needs selection and an active descendant, which Combobox, Command and Select provide.
- Each item has aria-posinset (index + 1) and aria-setsize (count), so a screen reader announces the position in the whole list, not the rendered window.
- The scroll container is in the Tab order so keyboard users can scroll it.
- A feed's articles are each focusable, and the feed is aria-busy only while busy is set (a page loading).
- A grid with columns is one Tab stop, the focused cell, and arrows move between cells, so every gridcell announced can be reached.
See also
- DataTable (opens in a new tab): A data grid: sort, search, filter, resize, reorder, pin, group, select, edit, page or virtualize, save views and export.