# VirtualizedList

> Renders very long lists efficiently by mounting only the visible window of items.

Category: Advanced · Status: stable · Package: @bindshq/stance-react@0.1.1 · Docs: https://stanceui.com/components/virtualized-list

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.

## Import

```tsx
import { VirtualizedList } from '@bindshq/stance-react';
```

Styles are imported once per app: `@bindshq/stance-tokens/css` and `@bindshq/stance-react/styles.css`.

## Examples

### Fixed height

```tsx
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}
    />
  );
}
```

### Selectable listbox

role="listbox" adds arrow-key navigation, selection and aria-activedescendant.

```tsx
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.

```tsx
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>
      )}
    />
  );
}
```

### Scroll to an item

```tsx
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

| Component | Prop | Type | Default | Description |
| --- | --- | --- | --- | --- |
| VirtualizedList | `items` (required) | `readonly T[]` | None | All items. Only the visible window is mounted. |
| VirtualizedList | `renderItem` (required) | `(item: T, index: number, state: { active, selected }) => ReactNode` | None | Item content. |
| VirtualizedList | `getItemKey` | `(item: T, index: number) => Key` | `index` | Stable key per item; also the selection key. |
| VirtualizedList | `itemHeight` | `number \| (index: number) => number` | None | Fixed height. Omit to measure items. |
| VirtualizedList | `estimatedItemHeight` | `number` | `40` | Estimate before measurement. |
| VirtualizedList | `height` | `number \| string` | `400` | Viewport height. |
| VirtualizedList | `overscan` | `number` | `4` | Items rendered beyond each edge. |
| VirtualizedList | `role` | `'list' \| 'listbox'` | `'list'` | Listbox enables keyboard selection. |
| VirtualizedList | `selectedKey / defaultSelectedKey` | `Key \| null` | None | Listbox selection, controlled or uncontrolled. |
| VirtualizedList | `onSelectionChange` | `(key, item) => void` | None | Called when an item is selected. |
| VirtualizedList | `isItemDisabled` | `(item: T, index: number) => boolean` | None | Disabled items are skipped by keyboard navigation. |
| VirtualizedList | `getItemLabel` | `(item: T, index: number) => string` | None | Accessible name for rich items. |
| VirtualizedList | `apiRef` | `Ref<VirtualizedListHandle>` | None | Imperative handle with scrollToIndex(index, align). |
| VirtualizedList | `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. |

## 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.

## Related

- https://stanceui.com/components/searchable-list
- https://stanceui.com/components/data-grid
- https://stanceui.com/components/list
