Virtual scroll
Windowed list — 10k rows, ~25 DOM nodes.
- Browsers
- Chrome 123+Firefox 121+Safari 17.5+
- Tier
- 2
- Bundle compressed
- 4.16 kB
- Forced layout budget
- 0 ms
- Axe budget
- 0
- SSR compatible
- Yes
- Published contract
- Test harness
Playground
Virtual scroll
10,000 team members with checkbox, avatar, and dropdown actions. Only visible rows (+ buffer) mount in the DOM — open DevTools and count [data-uiify-virtual-scroll] [data-part="row"] nodes while scrolling.
Horizontal
The same users as a horizontal strip with orientation="horizontal". rowHeight sets each card's width; the window follows the viewport width.
API
VirtualScroll
| Prop | Type | Default | Description |
|---|---|---|---|
items* | T[] | — | The full list of items, each with a stable id; only the rows in the current range are rendered. |
renderRow* | (item: T, index: number) => ReactNode | — | Renders the contents of one row from its item and absolute index. |
height | number | — | Explicit height in pixels. Omit it and pass style={{ blockSize: '100%' }} to fill a definite parent height; 600px is the default when neither is supplied. |
rowHeight | number | 64 | Fixed pixel size of each row on the scroll axis (height when vertical, width when horizontal), used to compute scroll size and visible rows. |
orientation | "vertical" | "horizontal" | "vertical" | Scroll axis. Horizontal lists scroll on the inline axis, size the window from the measured viewport width, mirror correctly in right-to-left layouts, and mirror the value to data-orientation. |
overscan | number | 5 | Number of extra rows rendered above and below the visible range so fast scrolling does not reveal blank space. |
estimateRowHeight | (index: number) => number | — | Per-index height estimate that switches the list to dynamic layout, where rendered rows are measured and the estimate is corrected. |
role | "list" | "grid" | "list" | ARIA role applied to the scroll container. |
anchor | "start" | "end" | "start" | Which end of the list the scroll position stays pinned to when items are added or removed. |
followAppend | boolean | "smooth" | false | Scrolls to newly appended items when the list is already at the end, optionally with smooth scrolling. |
scrollRef | Ref<VirtualScrollHandle> | — | Receives the imperative handle exposing scrollToIndex, scrollToOffset and scrollToEnd. |
onRangeChange | (range: VirtualScrollRangeChange) => void | — | Called with the visible and overscanned index ranges whenever the rendered window moves. |
onScrollStateChange | (state: VirtualScrollScrollState) => void | — | Called when scrolling starts and when it settles. |
isScrollingResetDelay | number | 150 | Milliseconds of quiet before scrolling is treated as settled in browsers without the scrollend event. |
className | string | — | Class applied to the scroll container. |
testId | string | — | Value for the scroll container's data-testid attribute. |
Keyboard
| Key | Action |
|---|---|
| Scroll | Wheel, trackpad, or scrollbar moves the window |
| Tab | Move focus between row controls (checkbox, menu trigger) |
| Arrow keys | Rove items inside an open dropdown menu |
| Escape | Close an open dropdown menu |
Accessibility
A full-height spacer keeps the native scrollbar accurate while only a fixed pool of row slots, plus overscan, is mounted; stable slot keys recycle DOM nodes. Rows expose role="listitem" with aria-posinset and aria-setsize inside a role="list" scrollport. Scroll sync is passive, and large jumps such as the scrollbar, Home/End or Cmd+Arrow update the range synchronously.
Styling hooks
data-uiify-virtual-scrolldata-part="spacer"data-part="row"