VirtualList
VirtualList is a high-performance virtualized list component for rendering large datasets efficiently using a fixed row height. It only renders visible rows, enabling smooth scrolling even with thousands of items.
Usage
Basic usage
import { VirtualList } from '@nofinite/nui';
const items = Array.from({ length: 2000 }, (_, i) => `Item ${i + 1}`);
<VirtualList
height={300}
items={items}
renderItem={(item) => <div>{item}</div>}
/>;
With interaction (selectable rows)
const [active, setActive] = useState<number | null>(null);
<VirtualList
height={300}
items={items}
renderItem={(item, index) => (
<div
onClick={() => setActive(index)}
style={{
background: active === index ? 'var(--color-primary)' : 'transparent',
color: active === index ? '#fff' : 'inherit',
borderRadius: 6,
padding: '4px 8px',
cursor: 'pointer',
}}
>
{item}
</div>
)}
/>;
Variants
VirtualList does not provide built-in visual variants.
Guidelines:
- Customize appearance via
renderItem. - Use
classNameto style the container. - Row-level styles should live inside
renderItem.
Sizes
VirtualList uses a fixed row height model.
<VirtualList itemHeight={28} />
<VirtualList itemHeight={36} />
<VirtualList itemHeight={48} />
Available sizes (via itemHeight):
- Any number (in px)
- Default:
36
States
This component does not manage internal UI states.
Common patterns:
- Empty list: pass an empty
itemsarray. - Active / selected row: manage externally via
renderItem. - Loading: render skeleton rows or placeholder content.
Native Props / Composition
VirtualList renders a scrollable <div> container and forwards styling via className.
<VirtualList
height={400}
items={items}
className="my-virtual-list"
renderItem={(item) => <div>{item}</div>}
/>
Custom row composition:
<VirtualList
height={420}
items={items}
renderItem={(item) => (
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<span>{item}</span>
</div>
)}
/>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | T[] | — | Full dataset to be virtualized |
height | number | — | Height of the scroll viewport (px) |
itemHeight | number | 36 | Fixed height of each row (px) |
renderItem | (item: T, index: number) => ReactNode | — | Function to render a single row |
className | string | "" | Additional class names for the container |
Behavior Notes
- Uses fixed row height for precise and fast calculations.
- Automatically overscans rows to avoid scroll flicker.
- Scroll handling is optimized using
useLayoutEffect. - Designed to handle 10k+ rows smoothly.
Example:
<VirtualList
height={400}
itemHeight={40}
items={items}
renderItem={(item, index) => (
<div>
{index}: {item}
</div>
)}
/>
Accessibility
- Renders as a scrollable
<div>. - Keyboard navigation should be handled inside
renderItemif needed. - Use semantic elements (
button,a, etc.) inside rows when required. - Add ARIA roles manually for complex list semantics if needed.
Example:
<VirtualList
height={300}
items={items}
renderItem={(item) => <button aria-label={`Select ${item}`}>{item}</button>}
/>
Layout
- Container is block-level and full-width by default.
- Vertical scrolling only (
overflow-y: auto). - Width is controlled by the parent container.
<div style={{ width: 320 }}>
<VirtualList height={400} items={items} renderItem={(i) => <div>{i}</div>} />
</div>
Best Practices
Do
- Use for large datasets (hundreds or thousands of rows).
- Keep
itemHeightconsistent with rendered row height. - Move all row styling and interaction into
renderItem.
Don’t
- Use variable-height rows (not supported).
- Attach heavy logic inside scroll handlers.
- Render complex layouts without memoization if performance matters.