mcpField
R
react-virtuoso

react-virtuoso

github.com/virtuoso-devDevelopment1213 installsUpdated 2d ago
Repository

About react-virtuoso

react-virtuoso is a React component library for virtualized lists, grids, and tables. It renders visible content and measures item sizes automatically with ResizeObserver, including variable-height items, for interfaces with large datasets.

Key features

  1. 01Virtualize lists, grouped lists, tables, and grids
  2. 02Measure variable item sizes automatically
  3. 03Support endless scrolling and prepending
  4. 04Provide programmatic scrolling and state restoration
  5. 05Use window scrolling or a custom scroll parent

Use cases

  1. 01Render lists or tables with many entries
  2. 02Build lists with sticky group headers
  3. 03Create responsive multi-column virtual grids
  4. 04Preserve scroll position when loading older entries

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/virtuoso-dev/skills --skill react-virtuoso

After installing, type/react-virtuosoin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

references/1.virtuoso/
auto-resizing.md
basic-usage.md
custom-scroll-container.md
customize-rendering.md
endless-scrolling.md
footer.md
horizontal-mode.md
initial-index.md
keyboard-navigation.md
press-to-load-more.md
range-change-callback.md
scroll-handling.md
scroll-seek-placeholders.md
scroll-to-index.md
tailwind-test.md
top-items.md
window-scrolling.md
references/2.grouped-virtuoso/
grouped-by-first-letter.md
grouped-numbers.md
grouped-with-load-on-demand.md
scroll-to-group.md
references/3.virtuoso-grid/
grid-responsive-columns.md
references/4.table-virtuoso/
basic-table.md
table-fixed-columns.md
table-fixed-headers.md
table-grouped.md
references/5.third-party-integration/
material-ui-endless-scrolling.md
mocking-in-tests.md
mui-table-virtual-scroll.md
tanstack-table-integration.md

Details

From the project README

react-virtuoso

react-virtuoso renders only the visible portion of large lists, grids, and tables. It measures item sizes automatically with ResizeObserver — variable item heights work out of the box, with no size configuration.

import { Virtuoso } from 'react-virtuoso'
;<Virtuoso style={{ height: '100%' }} data={users} itemContent={(index, user) => <div>{user.name}</div>} />

Picking the right component

NeedUse
Flat list, variable or fixed item heightsVirtuoso
Groups with sticky group headersGroupedVirtuoso
HTML table with virtualized rowsTableVirtuoso
Table with grouped rows and sticky group headersGroupedTableVirtuoso
Same-sized items in a responsive multi-column gridVirtuosoGrid
Chat / AI conversation UI (streaming, stick-to-bottom, prepend history)@virtuoso.dev/message-list — use the message-list skill instead
Data grid with columns, sorting, filtering, column features@virtuoso.dev/data-table — use the data-table skill instead

All components share the same core props (data/totalCount, itemContent, components, scroll callbacks, ref methods).

What you do NOT need to do

Unlike TanStack Virtual or react-window, react-virtuoso measures items itself. Do not carry those libraries' patterns over:

  • No estimateSize, no measureElement, no data-index wiring — measurement is automatic.
  • No absolute positioning or transform: translateY on items — the library positions items.
  • No fixed itemSize requirement — variable heights are the default. If items genuinely have one uniform height, pass fixedItemHeight as a performance optimization only.
  • No windowing math — pass data (or totalCount) and render the item in itemContent.

Core rules

  • The component needs a height. Set style={{ height: '100%' }} (with a sized parent) or a fixed height. A zero-height container renders nothing.
  • Never put CSS margins on items. ResizeObserver reports contentRect, which excludes margins, so the computed total height comes up short — the classic symptom is a list that cannot scroll all the way to the bottom. Use padding instead. Watch for default margins on <p>, headings, <ul>, <blockquote>, <pre>.
  • Use data, not totalCount, when you have the items. With data, itemContent={(index, item) => ...} receives the item. Use totalCount only when items are derived from the index. Updates must produce a new array reference.
  • Provide computeItemKey={(index, item) => item.id} whenever the list can be prepended, reordered, or filtered. The default key is the index, which remounts items (losing state) when positions shift.
  • Define components overrides outside the render function. Inline definitions create a new component type each render, remounting the subtree on every scroll. Scroller and List overrides must forward ref to their DOM element.
  • Item content must not render zero-height elements. The error "zero-sized element, this should not happen" means an item measured 0px — filter empty items out of the data instead.

Common patterns

Infinite scrolling
<Virtuoso data={items} endReached={() => loadMore()} itemContent={(index, item) => <Item item={item} />} />

endReached fires at the bottom; render a spinner via components.Footer. For "load more" on click, put the button in Footer.

Prepending items (reverse infinite scroll)

Prepending naively makes the list jump. Instead, keep a firstItemIndex that you decrease by the number of prepended items:

const [firstItemIndex, setFirstItemIndex] = useState(START)

const prepend = async () => {
  const older = await fetchOlderItems()
  setFirstItemIndex((i) => i - older.length)
  setItems((current) => [...older, ...current])
}

<Virtuoso
  computeItemKey={(_, item) => item.id}
  data={items}
  firstItemIndex={firstItemIndex}
  initialTopMostItemIndex={{ index: 'LAST' }}
  startReached={() => void prepend()}
  ...
/>

firstItemIndex must stay a positive number, so start it large (e.g. 100000). For GroupedVirtuoso, decrease it by the number of new items only, excluding the group headers. See endless-scrolling.

Following new items (logs, feeds)
<Virtuoso followOutput="smooth" data={messages} ... />

followOutput scrolls to new bottom items only when the user is already at the bottom. It accepts 'auto' | 'smooth' | false or a function (isAtBottom) => ... for custom logic. For full chat UIs prefer @virtuoso.dev/message-list.

Scrolling programmatically
const ref = useRef<VirtuosoHandle>(null)
ref.current?.scrollToIndex({ index: 500, align: 'center', behavior: 'smooth' })

Also on the handle: scrollIntoView (only scrolls if not visible — right for keyboard navigation), scrollTo/scrollBy (pixel-based), and getState (snapshot for restoreStateFrom when remounting, e.g. back navigation).

To start at an item, use initialTopMostItemIndex={{ index, align: 'start' }} — not initialScrollTop, which first renders at the top and then jumps.

Grouped lists
<GroupedVirtuoso
  groupCounts={[20, 30]} // items per group, in order
  groupContent={(groupIndex) => <Header group={groups[groupIndex]} />}
  itemContent={(index, groupIndex) => <Item item={items[index]} />} // index is absolute across all items
/>

You provide flat data plus groupCounts; map indexes back to your data yourself.

Tables
<TableVirtuoso
  data={rows}
  fixedHeaderContent={() => (
    <tr>
      <th>Name</th>
    </tr>
  )}
  itemContent={(index, row) => (
    <>
      <td>{row.name}</td>
    </>
  )} // <td> cells only — the row <tr> is rendered for you
/>

Do not set border-collapse: collapse on the table — the sticky header's borders scroll away with the body. Use border-collapse: separate with explicit cell borders. Customize structure via components (Table, TableRow, TableHead, TableBody).

Grid

VirtuosoGrid virtualizes same-sized items in columns. You control column count with CSS — give components.Item a percentage width (33% for three columns, changed via media queries) and components.List display: flex; flex-wrap: wrap. See grid-responsive-columns.

Page-level scrolling

Use useWindowScroll to drive the list from the document scroll, or customScrollParent={element} to attach to an existing scrollable ancestor. Pick exactly one scroll mode — combining them makes both containers scroll.

Testing

JSDOM has no layout, so items will not render in Jest/Vitest without mocked measurements:

render(<Virtuoso data={data} />, {
  wrapper: ({ children }) => (
    <VirtuosoMockContext.Provider value={{ viewportHeight: 300, itemHeight: 100 }}>{children}</VirtuosoMockContext.Provider>
  ),
})

Use VirtuosoGridMockContext (adds viewportWidth, itemWidth) for grids. Prefer real-browser tests (Playwright) for scroll behavior.

Troubleshooting

SymptomLikely causeFix
Nothing rendersContainer has zero heightGive the component or its parent a real height
Cannot scroll to the last items / jumps near the bottomMargins on item contentReplace margins with padding
Items flicker or lose state while scrollingInline components definitions or index-based keysHoist components; add computeItemKey
List jumps on prependItems added without firstItemIndex adjustmentUse the firstItemIndex prepend pattern
"zero-sized element, this should not happen"An item rendered with zero heightFilter empty items from the data
"ResizeObserver loop completed with undelivered notifications" overlay in devBenign ResizeObserver timing, surfaced by the dev-server overlayDisable runtimeErrors in the webpack/vite overlay config; safe to filter in error tracking
Hard-to-explain size behavior—Set logLevel={LogLevel.DEBUG} and watch the console with all levels enabled

Tuning: increaseViewportBy renders extra pixels outside the viewport (smoother, more DOM); defaultItemHeight skips the initial probe render; fixedItemHeight skips measurement entirely (uniform items only); scrollSeekConfiguration swaps items for placeholders during fast scrolling.

References

Detailed guides with full code in references/:

Full API reference: https://virtuoso.dev/react-virtuoso/

Comments

Markdown supported