From virtuoso-skills
Builds virtualized lists, grids, and tables with react-virtuoso. Handles variable heights, grouped lists, sticky headers, responsive grids, and chat-like feeds automatically.
How this skill is triggered — by the user, by Claude, or both
Slash command
/virtuoso-skills:react-virtuosoThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
`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.
references/1.virtuoso/auto-resizing.mdreferences/1.virtuoso/basic-usage.mdreferences/1.virtuoso/custom-scroll-container.mdreferences/1.virtuoso/customize-rendering.mdreferences/1.virtuoso/endless-scrolling.mdreferences/1.virtuoso/footer.mdreferences/1.virtuoso/horizontal-mode.mdreferences/1.virtuoso/initial-index.mdreferences/1.virtuoso/keyboard-navigation.mdreferences/1.virtuoso/press-to-load-more.mdreferences/1.virtuoso/range-change-callback.mdreferences/1.virtuoso/scroll-handling.mdreferences/1.virtuoso/scroll-seek-placeholders.mdreferences/1.virtuoso/scroll-to-index.mdreferences/1.virtuoso/tailwind-test.mdreferences/1.virtuoso/top-items.mdreferences/1.virtuoso/window-scrolling.mdreferences/2.grouped-virtuoso/grouped-by-first-letter.mdreferences/2.grouped-virtuoso/grouped-numbers.mdreferences/2.grouped-virtuoso/grouped-with-load-on-demand.mdreact-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>} />
| Need | Use |
|---|---|
| Flat list, variable or fixed item heights | Virtuoso |
| Groups with sticky group headers | GroupedVirtuoso |
| HTML table with virtualized rows | TableVirtuoso |
| Table with grouped rows and sticky group headers | GroupedTableVirtuoso |
| Same-sized items in a responsive multi-column grid | VirtuosoGrid |
| 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).
Unlike TanStack Virtual or react-window, react-virtuoso measures items itself. Do not carry those libraries' patterns over:
estimateSize, no measureElement, no data-index wiring — measurement is automatic.transform: translateY on items — the library positions items.itemSize requirement — variable heights are the default. If items genuinely have one uniform height, pass fixedItemHeight as a performance optimization only.data (or totalCount) and render the item in itemContent.style={{ height: '100%' }} (with a sized parent) or a fixed height. A zero-height container renders nothing.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>.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.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.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.<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 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.
<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.
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.
<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.
<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).
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.
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.
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.
| Symptom | Likely cause | Fix |
|---|---|---|
| Nothing renders | Container has zero height | Give the component or its parent a real height |
| Cannot scroll to the last items / jumps near the bottom | Margins on item content | Replace margins with padding |
| Items flicker or lose state while scrolling | Inline components definitions or index-based keys | Hoist components; add computeItemKey |
| List jumps on prepend | Items added without firstItemIndex adjustment | Use the firstItemIndex prepend pattern |
| "zero-sized element, this should not happen" | An item rendered with zero height | Filter empty items from the data |
| "ResizeObserver loop completed with undelivered notifications" overlay in dev | Benign ResizeObserver timing, surfaced by the dev-server overlay | Disable 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.
Detailed guides with full code in references/:
references/1.virtuoso/ — flat list guides: basic-usage, endless-scrolling, press-to-load-more, initial-index, scroll-to-index, keyboard-navigation, customize-rendering, footer, top-items, scroll-handling, range-change-callback, scroll-seek-placeholders, auto-resizing, window-scrolling, custom-scroll-container, horizontal-modereferences/2.grouped-virtuoso/ — grouped lists: grouped-numbers, grouped-by-first-letter, scroll-to-group, grouped-with-load-on-demandreferences/3.virtuoso-grid/ — grid-responsive-columnsreferences/4.table-virtuoso/ — tables: basic-table, table-fixed-headers, table-fixed-columns, table-groupedreferences/5.third-party-integration/ — mocking-in-tests, tanstack-table-integration, mui-table-virtual-scroll, material-ui-endless-scrollingFull API reference: https://virtuoso.dev/react-virtuoso/
npx claudepluginhub petyosi/react-virtuoso --plugin virtuoso-skillsVirtualizes MUI lists with react-window FixedSizeList/VariableSizeList, react-virtuoso, and Autocomplete patterns for rendering 1000+ items without layout thrashing or memory issues.
Optimizes large lists with virtual scrolling/windowing. Supports react-window, TanStack Virtual, variable-height rows, infinite scroll, and grid virtualization.
Virtualizes large lists, grids, and tables at 60FPS by rendering only visible items with headless UI for TS/JS, React, Vue, Solid, Svelte, Lit, Angular.