react-virtuoso
react-virtuoso is a family of React components that renders only the visible slice of a large collection. Virtuoso handles flat lists with variable item heights, GroupedVirtuoso adds sticky groups, VirtuosoGrid handles equally sized cards, and TableVirtuoso preserves table markup and sticky headers. It measures real DOM content with ResizeObserver, updates when item sizes change, and includes controls for infinite loading, reverse loading, window scrolling, scroll restoration, server rendering, and imperative navigation. It saves rendering work, not data-fetching or state-management work.
The easiest high-level choice for large, variably sized React lists and tables, with unusually good reverse-scroll and measurement behavior. Skip it for small lists, non-DOM targets, or when a headless primitive gives you the control you actually need.
Use it if
- You have hundreds or thousands of React rows whose heights vary and do not want to maintain a measurement cache yourself
- You need one package for virtual lists, grouped lists, responsive equal-size grids, and semantic tables
- You are building feeds or log viewers that load at either edge, preserve scroll position, or follow appended output only when the reader is already at the bottom
- You need React 16 through 19 support with TypeScript declarations, ESM and CommonJS builds, and no runtime dependencies
- Your list is only a few dozen cheap rows: normal array rendering is simpler, easier to test, fully searchable by the browser, and avoids an 18.6 KB gzipped dependency
- You target React Native or another non-DOM renderer: item sizing depends on browser layout and ResizeObserver, and the package's components emit DOM list, grid, or table structures
- You need a variable-height masonry grid: the VirtuosoGrid documentation explicitly requires equally sized items, while @virtuoso.dev/masonry is a separate package
- You expect the advertised chat UI to be included under MIT: @virtuoso.dev/message-list is a separate package distributed under an annual commercial developer license and requires a license key
- You cannot control item layout: the troubleshooting guide says vertical margins make measurements too small, zero-height items are unsupported and throw, and late-loading images or iframes can make reverse scrolling jump
Setup reality
Install react-virtuoso alongside React and React DOM. Version 4.18.11 has no runtime dependencies and its peer range covers React 16 through 19; ESM, CommonJS, and TypeScript declarations are included. There is no stylesheet, provider, native build, credential, or config file. The first surprise is layout: a Virtuoso needs a real height through its style, class, flex parent, window scrolling, or custom scroll parent. A percentage height works only when ancestors establish one. The component uses the first rendered item as a height probe, so an unusually tall or short first row can require extra render passes; defaultItemHeight supplies a better estimate, fixedItemHeight disables measurement only when every row is truly fixed, and heightEstimates can seed irregular known sizes. ResizeObserver tracks later changes, but content still needs sane geometry. Remove vertical margins from items and common children such as p, headings, lists, blockquotes, and pre; use padding instead because measured content rectangles exclude margins. Filter zero-height records rather than hiding them inside the list. Give data items stable keys with computeItemKey when insertion or reordering occurs. Infinite loading callbacks do not fetch, deduplicate, cancel, or show errors for you, so guard endReached and startReached against concurrent requests. Prepending requires decreasing a positive firstItemIndex by exactly the number of inserted data items, not by group header count. SSR needs initialItemCount to emit an initial slice before browser measurements; it is not a fixed page size. JSDOM has no useful element measurements, so tests otherwise render no rows and must use VirtuosoMockContext or VirtuosoGridMockContext. Custom component types should be defined outside the parent render and ref-capable wrappers must forward their ref, or scrolling can cause remounts and lost state. React Virtuoso itself is MIT. The separately promoted Message List adds chat-specific data management under a commercial license, so budget and license that product independently if plain Virtuoso's followOutput and prepend primitives are not enough.
Patterns
Render a typed variable-height listrender-data-list
import {Virtuoso} from 'react-virtuoso';
type User = {id: string; name: string; bio: string};
export function UserList({users}: {users: User[]}) {
return (
<Virtuoso
style={{height: 480}}
data={users}
computeItemKey={(_index, user) => user.id}
itemContent={(_index, user) => (
<article style={{padding: 12}}>
<strong>{user.name}</strong>
<p style={{margin: 0}}>{user.bio}</p>
</article>
)}
/>
);
}data infers totalCount. Use a stable domain key and padding instead of vertical margins so insertions and measurements stay correct.
Render by index without materializing datarender-indexed-list
<Virtuoso
style={{height: 400}}
totalCount={100_000}
defaultItemHeight={36}
itemContent={(index) => <div style={{padding: 8}}>Row {index}</div>}
/>defaultItemHeight avoids using an outlier first row as the initial estimate. Use fixedItemHeight only when every rendered row really has that height.
Load another page at the bottomload-more-items
const [loading, setLoading] = useState(false);
const loadMore = useCallback(async () => {
if (loading || !hasNextPage) return;
setLoading(true);
try {
const next = await fetchNextPage();
setItems((current) => [...current, ...next]);
} finally {
setLoading(false);
}
}, [loading, hasNextPage]);
return <Virtuoso style={{height: 500}} data={items} endReached={loadMore} itemContent={renderItem} />;endReached can fire again as data and viewport size change. Guard concurrent requests and deduplicate records in application state.
Scroll to an item through the handlescroll-to-item
import {useRef} from 'react';
import {Virtuoso, type VirtuosoHandle} from 'react-virtuoso';
const listRef = useRef<VirtuosoHandle>(null);
<button onClick={() => listRef.current?.scrollToIndex({
index: 500,
align: 'center',
behavior: 'smooth',
})}>
Go to row 500
</button>
<Virtuoso ref={listRef} style={{height: 500}} totalCount={1000} itemContent={(index) => <div>{index}</div>} />Current releases clamp out-of-range indexes. Smooth scrolling across many unmeasured variable-height rows can still be visually expensive.
Start near a known itemset-initial-index
<Virtuoso
style={{height: 500}}
data={messages}
initialTopMostItemIndex={{index: messages.length - 1, align: 'end'}}
itemContent={renderMessage}
/>initialTopMostItemIndex is for mount-time placement. Use scrollToIndex for later changes, and avoid initialScrollTop when an item index is available.
Follow new output only at the bottomfollow-appended-output
<Virtuoso
style={{height: 500}}
data={messages}
alignToBottom
followOutput={(isAtBottom) => isAtBottom ? 'smooth' : false}
itemContent={(_index, message) => <MessageRow message={message} />}
/>This primitive is in the MIT package, but the turnkey Virtuoso Message List data API is a different commercially licensed package.
Prepend records without losing positionprepend-older-items
const INITIAL_INDEX = 100_000;
const [firstItemIndex, setFirstItemIndex] = useState(INITIAL_INDEX);
async function loadOlder() {
const older = await fetchOlder();
setFirstItemIndex((value) => value - older.length);
setMessages((current) => [...older, ...current]);
}
return (
<Virtuoso
style={{height: 500}}
firstItemIndex={firstItemIndex}
data={messages}
startReached={loadOlder}
itemContent={renderMessage}
/>
);firstItemIndex must stay positive and decrease by exactly the number of data items prepended. Guard startReached against overlapping fetches.
Render sticky groupsrender-grouped-list
import {GroupedVirtuoso} from 'react-virtuoso';
<GroupedVirtuoso
style={{height: 500}}
groupCounts={[3, 2, 4]}
groupContent={(groupIndex) => (
<div style={{background: 'white', padding: 8}}>{groupNames[groupIndex]}</div>
)}
itemContent={(itemIndex) => <ContactRow contact={contacts[itemIndex]} />}
/>groupCounts contains item counts per group, while itemContent receives the flattened item index rather than an index local to its group.
Render an equal-size responsive gridrender-responsive-grid
import {VirtuosoGrid} from 'react-virtuoso';
<VirtuosoGrid
style={{height: 600}}
data={products}
computeItemKey={(_index, product) => product.id}
listClassName="product-grid"
itemClassName="product-grid-item"
itemContent={(_index, product) => <ProductCard product={product} />}
/>Define the columns and item dimensions in CSS. VirtuosoGrid expects equal-size items; use a masonry-specific package for uneven card heights.
Render a table with a sticky headerrender-virtual-table
import {TableVirtuoso} from 'react-virtuoso';
<TableVirtuoso
style={{height: 520}}
data={users}
fixedHeaderContent={() => (
<tr><th>Name</th><th>Email</th></tr>
)}
itemContent={(_index, user) => (
<>
<td>{user.name}</td>
<td>{user.email}</td>
</>
)}
/>Keep normal table display and overflow behavior. border-collapse: collapse can make sticky-header borders scroll away; the docs recommend separate borders instead.
Emit an initial slice during SSRrender-on-server
<Virtuoso
style={{height: 500}}
data={articles}
initialItemCount={12}
itemContent={(_index, article) => <ArticleRow article={article} />}
/>initialItemCount renders rows without measurements for server output. The browser measures after hydration, so reserve stable item space to reduce layout shifts.
Render virtual rows in JSDOM testsmock-list-measurements
import {render} from '@testing-library/react';
import {Virtuoso, VirtuosoMockContext} from 'react-virtuoso';
const result = render(<Virtuoso data={users} itemContent={renderUser} />, {
wrapper: ({children}) => (
<VirtuosoMockContext.Provider value={{viewportHeight: 300, itemHeight: 50}}>
{children}
</VirtuosoMockContext.Provider>
),
});
expect(result.getByText(users[0].name)).toBeVisible();JSDOM does not provide usable layout measurements, so an unmocked Virtuoso commonly renders no items. Grid tests use VirtuosoGridMockContext with width values too.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @tanstack/react-virtual | npm | You want a headless virtualizer and are willing to own all markup, scrolling containers, and layout details |
| react-window | npm | A small fixed-size list or grid needs a narrower API and you can provide item dimensions |
| react-virtualized | npm | A legacy application already depends on its large suite of list, grid, table, and measurement components |
| @virtuoso.dev/masonry | npm | Cards have different heights and must pack into masonry columns rather than an equal-size VirtuosoGrid |