Components Advanced
VirtualizedList
StableRenders very long lists efficiently by mounting only the visible window of items.
Use VirtualizedList for thousands of items, such as audit logs, mailbox lists or recipients. Items can have a fixed height or be measured as they render. It can also act as a keyboard-selectable listbox. For short lists, a plain List is simpler and better for find-in-page.
Overview
import { VirtualizedList } from '@bindshq/stance-react';
const events = Array.from({ length: 5000 }, (_, i) => ({
id: `e${i}`,
text: `Signature deployed to mailbox ${String(i + 1).padStart(4, '0')}@binds.si`,
}));
export function Basic() {
return (
<VirtualizedList
aria-label="Deployment log"
items={events}
itemHeight={40}
height={280}
getItemKey={(e) => e.id}
renderItem={(e) => e.text}
/>
);
}Light, dark and high contrast
Every Stance component is designed and tested in all three appearance modes.
Import
import { VirtualizedList } from '@bindshq/stance-react';Examples
Selectable listbox
role="listbox" adds arrow-key navigation, selection and aria-activedescendant.
Selected: none
import * as React from 'react';
import { VirtualizedList } from '@bindshq/stance-react';
const people = [
'Priya Nair',
'Amara Okafor',
'Lukas Brenner',
'Hana Sato',
'Tomás Ruiz',
'Owen Price',
'Zainab Haddad',
'Mateja Kovač',
];
const recipients = Array.from({ length: 2000 }, (_, i) => ({
id: `r${i}`,
name: `${people[i % 8]} (${i + 1})`,
}));
export function Listbox() {
const [selected, setSelected] = React.useState<React.Key | null>(null);
return (
<div style={{ display: 'grid', gap: 8 }}>
<VirtualizedList
role="listbox"
aria-label="Choose a recipient"
items={recipients}
itemHeight={40}
height={240}
getItemKey={(r) => r.id}
selectedKey={selected}
onSelectionChange={setSelected}
renderItem={(r) => r.name}
/>
<p style={{ margin: 0 }}>
Selected: {selected ? recipients.find((r) => r.id === selected)?.name : 'none'}
</p>
</div>
);
}Measured heights
Omit itemHeight to measure rich items as they render.
Updated the legal disclaimer on the Sales template and requested a review from Legal before Friday’s rollout.
Approved the signature.
Approved the signature.
Updated the legal disclaimer on the Sales template and requested a review from Legal before Friday’s rollout.
Approved the signature.
Approved the signature.
Updated the legal disclaimer on the Sales template and requested a review from Legal before Friday’s rollout.
Approved the signature.
Approved the signature.
import { Avatar, VirtualizedList } from '@bindshq/stance-react';
const people = [
'Priya Nair',
'Amara Okafor',
'Lukas Brenner',
'Hana Sato',
'Tomás Ruiz',
'Owen Price',
'Zainab Haddad',
'Mateja Kovač',
];
export function MeasuredHeights() {
const notes = Array.from({ length: 300 }, (_, i) => ({
id: `n${i}`,
name: people[i % 8]!,
note:
i % 3 === 0
? 'Updated the legal disclaimer on the Sales template and requested a review from Legal before Friday’s rollout.'
: 'Approved the signature.',
}));
return (
<VirtualizedList
aria-label="Review notes"
items={notes}
height={300}
estimatedItemHeight={64}
getItemKey={(n) => n.id}
renderItem={(n) => (
<div style={{ display: 'flex', gap: 12, padding: '12px 0', alignItems: 'flex-start' }}>
<Avatar name={n.name} size="sm" />
<div>
<strong>{n.name}</strong>
<p style={{ margin: '2px 0 0' }}>{n.note}</p>
</div>
</div>
)}
/>
);
}import * as React from 'react';
import { Button, VirtualizedList } from '@bindshq/stance-react';
import type { VirtualizedListHandle } from '@bindshq/stance-react';
const events = Array.from({ length: 5000 }, (_, i) => ({
id: `e${i}`,
text: `Signature deployed to mailbox ${String(i + 1).padStart(4, '0')}@binds.si`,
}));
export function ScrollToIndex() {
const api = React.useRef<VirtualizedListHandle>(null);
return (
<div style={{ display: 'grid', gap: 12 }}>
<div style={{ display: 'flex', gap: 8 }}>
<Button size="sm" variant="outline" onClick={() => api.current?.scrollToIndex(0, 'start')}>
First
</Button>
<Button
size="sm"
variant="outline"
onClick={() => api.current?.scrollToIndex(2499, 'center')}
>
Middle
</Button>
<Button size="sm" variant="outline" onClick={() => api.current?.scrollToIndex(4999, 'end')}>
Last
</Button>
</div>
<VirtualizedList
aria-label="Deployment log"
items={events}
itemHeight={40}
height={240}
apiRef={api}
renderItem={(e) => e.text}
/>
</div>
);
}API reference
| Prop | Type | Default | Description |
|---|---|---|---|
items* required | readonly T[] | None | All items. Only the visible window is mounted. |
renderItem* required | (item: T, index: number, state: { active, selected }) => ReactNode | None | Item content. |
getItemKey | (item: T, index: number) => Key | index | Stable key per item; also the selection key. |
itemHeight | number | (index: number) => number | None | Fixed height. Omit to measure items. |
estimatedItemHeight | number | 40 | Estimate before measurement. |
height | number | string | 400 | Viewport height. |
overscan | number | 4 | Items rendered beyond each edge. |
role | 'list' | 'listbox' | 'list' | Listbox enables keyboard selection. |
selectedKey / defaultSelectedKey | Key | null | None | Listbox selection, controlled or uncontrolled. |
onSelectionChange | (key, item) => void | None | Called when an item is selected. |
isItemDisabled | (item: T, index: number) => boolean | None | Disabled items are skipped by keyboard navigation. |
getItemLabel | (item: T, index: number) => string | None | Accessible name for rich items. |
apiRef | Ref<VirtualizedListHandle> | None | Imperative handle with scrollToIndex(index, align). |
emptyState | ReactNode | None | Shown when there are no items. |
Accessibility
- Each rendered item reports aria-setsize and aria-posinset, so screen readers announce “12 of 10,000” even though most items are not in the DOM.
- The scroll container is focusable, so keyboard users can scroll a plain list with the arrow keys.
- In listbox mode, focus stays on the container and aria-activedescendant points at the active option; the active option is scrolled into view.
- Find-in-page cannot see unmounted items; pair long lists with SearchableList or a filter.
Keyboard
| Keys | Action |
|---|---|
| ↑ ↓ | Listbox: move the active option (disabled options are skipped). |
| Home End | Listbox: first or last option. |
| Page Up Page Down | Listbox: move by a visible page. |
| Enter Space | Listbox: select the active option. |
Usage guidelines
Do
- Use fixed heights when possible; it is the fastest mode.
- Provide a search or filter for long lists.
Don’t
- Virtualise lists of a few dozen items.
- Nest focusable controls in listbox options.