LayoutReact
A scroller that renders only the rows near the scroll position, so a list of 10,000 items keeps a few dozen in the DOM.
Rendered by React 19.3.1
Installation
npm i @hamur/reactNot on npm yet. The package goes out under the chosen name.
Anatomy
Import the component and assemble its parts:
<Virtualizer />Examples
Listbox
A Listbox of 5,000 options; End, Home, Page Down and typeahead reach options that are not mounted.
Data Table
A Data Table grid of 1,000 rows and 40 columns with layout="table" and columnCount; cells are placed by --column-offset.
Chat
A role="log" chat with anchor="end"; onStartReached prepends 30 older messages and the first visible message stays in place.
Scroll To Index
A row number and an alignment; the button scrolls through actionsRef to rows that were never measured.
API reference
35 props across 1 parts, in React syntax. className, style, render are listed once, under Shared props.
VirtualizerVirtualizer
Renders only the rows of a long collection that are near the scroll position, keeping the focused, active and kept rows mounted. Inside Listbox, Combobox, Autocomplete, Select, Tree, Tree Select, Cascader, Grid List or DataTable it windows that component's items. (a <tbody> in the table layout). Not in upstream's release (mui/base-ui#5466). Renders a <div>.
| Prop | Type | Default |
|---|---|---|
itemsThe items, flat or as groups of { items }. | readonly Item[] | readonly VirtualizerGroup<Item>[] | — |
getItemKeyReturns a stable key for an item. | ((item: Item, index: number) => string | number) | — |
childrenrequiredRenders the row of an item. | (item: Item, index: number, props: VirtualizerItemProps) => React.ReactNode | — |
itemHeightThe height of every row, in px; rows are then not measured. | number | — |
itemWidthThe width of every item of a horizontal list, in px; items are then not measured. | number | — |
estimatedItemHeightThe height of a row before it is measured: a number, refined by the average of the measured | VirtualizerSizeEstimate | 32 |
estimatedItemWidthThe width of an item of a horizontal list before it is measured. | VirtualizerSizeEstimate | — |
orientationThe scroll axis. | VirtualizerOrientation | 'vertical' |
layoutlist wraps each row; table renders <tr> rows in a <tbody>; grid renders rows of | VirtualizerLayout | 'list' |
columnsItems per row in the grid layout. | number | — |
columnCountThe number of columns, which windows the columns of the table and grid layouts. | number | — |
columnWidthThe width of every column, in px, or a function of the column index. | VirtualizerSizeEstimate | — |
estimatedColumnWidthThe width of a column, in px, without columnWidth. | VirtualizerSizeEstimate | 120 |
columnOverscanPxThe width rendered beyond the visible columns, in px. | number | — |
overscanPxThe size rendered beyond the viewport, in px. | number | 150 |
activeIndexThe active item, always mounted. | number | { index: number; scroll?: boolean | undefined; align?: VirtualizerAlign | undefined; paddingStart?: number | undefined; paddingEnd?: number | undefined; } | — |
keepMountedIndices kept mounted outside the window, off-screen and exposed. | readonly number[] | — |
totalItemsThe size of the whole set for aria-setsize; -1 when unknown. | number | — |
itemAriaThe ARIA each row carries: set (aria-posinset, aria-setsize), row | VirtualizerItemAria | — |
onEndReachedCalled once each time the end of the list comes within endReachedThreshold. | (() => void) | — |
endReachedThresholdThe distance from the end, in px, at which onEndReached is called. | number | 0 |
onStartReachedCalled once each time the start of the list comes within startReachedThreshold. | (() => void) | — |
startReachedThresholdThe distance from the start, in px, at which onStartReached is called. | number | 0 |
anchorend starts at the end, follows appended items while at the end, and keeps the first | "start" | "end" | 'start' |
scrollElementThe element that scrolls: the Virtualizer itself, the window, an element, or null for the | VirtualizerScrollElement | 'self' |
initialIndexThe item scrolled to on mount, and rendered first on the server. | number | — |
initialItemCountThe number of rows rendered on the server and before the first measurement. | number | 15 |
initialSnapshotA snapshot from actionsRef.current.getSnapshot(), restored on mount. | VirtualizerSnapshot | — |
onRangeChangeCalled with the rendered range (end exclusive) when it changed and the scroll settled. | ((range: VirtualizerRange) => void) | — |
renderGroupHeaderRenders the header of a group. | ((group: VirtualizerGroup<Item>, groupIndex: number, props: { ref?: ((element: Element | null) => void) | undefined; hidden?: boolean | undefined; id: string; 'aria-hidden': true; }) => React.ReactNode) | — |
getGroupKeyReturns a stable key for a group. | ((group: VirtualizerGroup<Item>, groupIndex: number) => string | number) | — |
estimatedGroupHeaderHeightThe height of a group header before it is measured. | VirtualizerSizeEstimate | — |
trailingContent after the last row, such as a loading indicator. | React.ReactNode | — |
enabledWhether rows are windowed. | boolean | true |
actionsRefA ref to imperative actions. | React.Ref<VirtualizerActions> | — |
| Data attribute | Description |
|---|---|
data-empty | Present when the collection has no items. |
data-orientation | Indicates the scroll axis. |
| CSS variable | Description |
|---|---|
--total-size | The size of every row along the scroll axis, in pixels. |
--total-column-size | The width of every column, in pixels, with column windowing. |
--column-offset | Set on a row's props.style: the start offset of its first rendered column, in pixels. |
Shared props
Every part that renders an element takes these.
| Prop | Type |
|---|---|
classNameCSS class applied to the element, or a function that
returns a class based on the component's state. | string | ((state: State) => string | undefined) |
styleStyle applied to the element, or a function that
returns a style object based on the component's state. | React.CSSProperties | ((state: State) => React.CSSProperties | undefined) |
renderAllows you to replace the component's HTML element
with a different tag, or compose it with another component. | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State> |
Keyboard and accessibility
Keyboard
| Keys | Does | Part |
|---|---|---|
| HomeEnd | In a host, move to the first or last item, which is mounted and scrolled to first | Virtualizer |
| PageDownPageUp | In a host, move 10 items, past the rows that are mounted | Virtualizer |
| In a host with typeahead, moves to the next item whose text starts with it, mounted or not | Virtualizer | |
| ArrowDownArrowUp | In a Grid List with columns, move by rows across rows that are not mounted | Virtualizer |
| Tab | From a focused row scrolled out of view, moves to the control after the list | Virtualizer |
| ShiftTab | From a focused row scrolled out of view, moves to the control before the list | Virtualizer |
Accessibility
| Part | Sets | So that |
|---|---|---|
| Virtualizer | role="presentation" on the spacer, the window and each row wrapper, and no role, tabindex or aria-busy on itself | the list's own role owns its rows, and the app or the host names the pattern |
| Virtualizer | aria-setsize and aria-posinset in each row's props with itemAria set, the default of a list; aria-rowindex with row, the default of the table and grid layouts | a row is announced at its place in the whole collection (totalItems), not in the rendered slice |
| Virtualizer | the focused row, the activeIndex row and the keepMounted rows stay mounted outside the window, off-screen and in index order, never hidden, aria-hidden or inert | focus and aria-activedescendant never point at a removed element, and the DOM order is the index order |
| Virtualizer | role="group" with aria-labelledby around each group's rows, and aria-hidden on the header it names | a grouped row is announced with its group, and the header is not read twice |
Focus order
- The rows are the tab stops when they are focusable; the host's roving focus or the app's tabindex decides which one. A list of plain content makes the scroller the tab stop.
- A focused row that scrolls out of view stays mounted off-screen, so Tab and Shift+Tab leave the list from it to the controls after and before the list.
- In a host, the keys move focus or the highlight over the whole collection; the target row is mounted and scrolled into view before it takes focus.