mrkeyoor.com_
Sat 08 Aug 22:51 UTC
npmWeb Frontendupdated 08 Aug 2026

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.

Verdict

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.

API stability4/5Version 4 has accumulated capabilities through additive props and focused patch releases while keeping the central data, totalCount, itemContent, components, and imperative-handle model recognizable. The package ships one explicit root export with types for both module systems. Virtualization behavior is still sensitive by nature: recent patches changed index clamping, RTL positioning, window-scroll SSR, bottom detection, and React 19 subscriptions, so visual regression tests should accompany upgrades.
Docs5/5virtuoso.dev provides live, typed examples for flat, grouped, grid, table, reverse, endless, window-scrolling, keyboard, custom-component, and test scenarios, plus a generated API reference. The troubleshooting page is unusually direct about margins, zero-sized elements, remounting, ResizeObserver reports, and dynamic content. Pricing pages also separate the MIT core from the commercially licensed Message List instead of burying the distinction.
Maintenance5/5Version 4.18.11 shipped on July 17, 2026, and the repository was pushed on August 4. The 4.18 line includes fixes for React 19 subscription behavior, window-scroll server layout, RTL horizontal lists, bottom detection, out-of-range scroll indexes, and TypeScript table declarations. The repository is not archived and GitHub reports 55 open issues and pull requests combined, a manageable visible queue for a browser-layout component with broad behavior.
Ecosystem5/5The package recorded 3,135,551 downloads in the measured week, has 6,444 GitHub stars, and declares compatibility from React 16 through React 19. Official examples cover Material UI and TanStack Table, and standard DOM markup makes ordinary styling systems usable. The core adds no runtime dependencies and publishes both ESM and CommonJS, though specialized chat and masonry experiences now extend into separate packages.

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
Skip it if

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

PackageRegistryPick it when
@tanstack/react-virtualnpmYou want a headless virtualizer and are willing to own all markup, scrolling containers, and layout details
react-windownpmA small fixed-size list or grid needs a narrower API and you can provide item dimensions
react-virtualizednpmA legacy application already depends on its large suite of list, grid, table, and measurement components
@virtuoso.dev/masonrynpmCards have different heights and must pack into masonry columns rather than an equal-size VirtuosoGrid