Skip to main content

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 className to 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 items array.
  • 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

PropTypeDefaultDescription
itemsT[]Full dataset to be virtualized
heightnumberHeight of the scroll viewport (px)
itemHeightnumber36Fixed height of each row (px)
renderItem(item: T, index: number) => ReactNodeFunction to render a single row
classNamestring""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 renderItem if 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 itemHeight consistent 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.