Components
Virtual List
A long list that renders only the rows near the viewport, in islands: a <ul> whose rows are <li>s placed by createVirtualizer, each keyed by itemKey and carrying aria-posinset and aria-setsize. The children function renders a row's content once; a row kept under its key keeps its DOM and state while its item updates in place. The server renders the first initial rows in flow. 2.2 kB brotli with the virtualizer.
- 1. Person 1
- 2. Person 2
- 3. Person 3
- 4. Person 4
- 5. Person 5
- 6. Person 6
- 7. Person 7
- 8. Person 8
- 9. Person 9
- 10. Person 10
- 11. Person 11
- 12. Person 12
- 13. Person 13
- 14. Person 14
- 15. Person 15
- 16. Person 16
- 17. Person 17
- 18. Person 18
- 19. Person 19
- 20. Person 20
import { VirtualList } from "@lumiose/ui"; // or "@lumiose/ui/virtualizer"#Anatomy
<VirtualList items={people()} itemKey={(person) => person.id} size={40}>
{(row) => <a href={`/people/${row.item.id}`}>{row.item.name}</a>}
</VirtualList>| Part | Renders |
|---|---|
VirtualList |
<ul>, total() tall and position: relative once mounted; an <li data-row aria-posinset aria-setsize> per row |
#API
#VirtualList
| Prop | Type | Default | |
|---|---|---|---|
items |
readonly T[] |
The items. | |
itemKey |
(item: T) => string | number |
The item's identity, such as its record's id: a row stays while its key does. | |
children |
(row: VirtualItem<T>) => JSX.Element |
A row's content, rendered once per row. row.item and row.index are getters: read them where they are used. |
|
size |
number | (row: number) => number |
A row's height in px: fixed (the <li> gets it), or with measure the estimate. |
|
measure |
boolean |
false |
Measures the rendered rows (one ResizeObserver): their heights replace size. |
gap |
number |
0 |
The px between rows. |
overscan |
number |
4 |
Rows rendered past each edge of the viewport. |
scroller |
() => Element | null | undefined |
window | The element that scrolls. |
initial |
number |
0 |
The rows rendered before mount: the server's HTML. |
keepFocused |
boolean |
true |
Keeps the row that holds focus rendered while the list scrolls away from it. |
itemProps |
(item: T, index: number) => li props |
Attributes of each <li>: data attributes, a title. Not style, which places the row. |
|
virtualizer |
(virtualizer: Virtualizer) => void |
Receives the virtualizer, for scrollToIndex() and range(). |
Every other <ul> prop passes through: class, aria-label, aria-labelledby, id. A style
object applies too, with the list's own properties (its padding, position and height) over it.
#Example
A thousand people in a scroll box, the first 20 in the server's HTML:
import { VirtualList, type Virtualizer } from "@lumiose/ui";
const PEOPLE = Array.from({ length: 1000 }, (_, i) => ({
id: `p${i + 1}`,
name: `Person ${i + 1}`,
}));
export function PeopleList() {
let box: HTMLDivElement | undefined;
let list: Virtualizer | undefined;
return (
<>
<button type="button" onClick={() => list?.scrollToIndex(499, { align: "start" })}>
Go to person 500
</button>
<div
ref={(el) => (box = el)}
role="region"
aria-label="People"
tabindex={0}
style={{ "inline-size": "100%", "max-block-size": "16rem", overflow: "auto" }}
>
<VirtualList
aria-label="People"
style={{ margin: "0", padding: "0", "list-style": "none" }}
items={PEOPLE}
itemKey={(person) => person.id}
size={40}
initial={20}
scroller={() => box}
virtualizer={(v) => (list = v)}
>
{(row) => (
<span>
{row.index + 1}. {row.item.name}
</span>
)}
</VirtualList>
</div>
</>
);
}#Styling
Unstyled. Before mount the rows are in flow (each size tall, the rest of the list's height a
padding-block-end), so the server's list already has its full height. Once mounted the <ul>
gets data-mounted, position: relative and its total height, and each <li> position: absolute, inset: 0 0 auto and a transform. Style with class on the list, [data-mounted],
and the rows through the list's class: they are the component's elements, so a scoped selector
reaches them with :deep(), as in .people :deep(li). The content you render is yours to style.
#Keyboard and focus
The rows are in DOM order, so Tab moves through them, and the browser scrolls the next one into
view. A focused row stays rendered while the list scrolls away from it (keepFocused, on by default for placed rows, decision-84), so focus
never falls back to the page. Rows are keyed by the record: a row's focus, typed text or toggled
state stays with its record when items are added, removed, reordered or fetched again.
#Without JavaScript
The server renders the first initial rows, in flow, with the list's full height reserved. See
Virtualizer for the takeover and for lists that must be
complete in the HTML.
#Browser support
ResizeObserver (for measure): every supported browser.
#Related
- Virtualizer: the primitive the list runs on, for other layouts.
- Virtual Grid and Virtual Table.