Virtual List
React data-displayGeneric windowed scroller. Renders only visible items plus a small overscan, with fixed or dynamic item sizes. Use for long lists where most rows are off-screen.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://react.uipkge.dev/r/react/virtual-list.json$ npx shadcn@latest add https://react.uipkge.dev/r/react/virtual-list.json$ yarn dlx shadcn@latest add https://react.uipkge.dev/r/react/virtual-list.json$ bunx shadcn@latest add https://react.uipkge.dev/r/react/virtual-list.json
Or with the named registry:
npx shadcn@latest add @uipkge-react/virtual-list
Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
items | T[] | — | required |
itemSize Fixed pixel size, or a measurer called per item/index. | number | ((item: T, index: number) => number) | — | required |
height | number | string | — | required |
overscan | number | — | optional |
keyField | keyof T | string | — | optional |
direction | 'vertical' | 'horizontal' | — | optional |
className | string | — | optional |
children Render-prop for each row. | (item: T, index: number) => React.ReactNode | — | required |
onScroll | (event: React.UIEvent<HTMLDivElement>) => void | — | optional |
onRangeChange | (range: [number, number]) => void | — | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
VirtualListHandle interface VirtualListHandle {
scrollToOffset: (px: number) => void
scrollToIndex: (index: number, options?: { align?: 'start' | 'center' | 'end' }) => void
getVisibleRange: () => [number, number]
} Dependencies
Files (2)
-
components/ui/virtual-list/virtual-list.tsx 4.1 kB
'use client' import * as React from 'react' import { useVirtualizer } from '@tanstack/react-virtual' import { cn } from '@/lib/utils' export interface VirtualListHandle { scrollToOffset: (px: number) => void scrollToIndex: (index: number, options?: { align?: 'start' | 'center' | 'end' }) => void getVisibleRange: () => [number, number] } export interface VirtualListProps<T> { items: T[] /** Fixed pixel size, or a measurer called per item/index. */ itemSize: number | ((item: T, index: number) => number) height: number | string overscan?: number keyField?: keyof T | string direction?: 'vertical' | 'horizontal' className?: string /** Render-prop for each row. */ children: (item: T, index: number) => React.ReactNode onScroll?: (event: React.UIEvent<HTMLDivElement>) => void onRangeChange?: (range: [number, number]) => void } function VirtualListInner<T extends Record<string, any>>( { items, itemSize, height, overscan = 3, keyField = 'id', direction = 'vertical', className, children, onScroll, onRangeChange, }: VirtualListProps<T>, ref: React.Ref<VirtualListHandle>, ) { const scrollRef = React.useRef<HTMLDivElement | null>(null) const isVertical = direction === 'vertical' const virtualizer = useVirtualizer({ count: items.length, horizontal: !isVertical, getScrollElement: () => scrollRef.current, estimateSize: (index) => typeof itemSize === 'function' ? itemSize(items[index]!, index) : itemSize, overscan, getItemKey: (index) => { const item = items[index] const k = item?.[keyField as keyof T] return (k ?? index) as string | number }, }) const virtualItems = virtualizer.getVirtualItems() const totalSize = virtualizer.getTotalSize() React.useEffect(() => { if (!onRangeChange || virtualItems.length === 0) return onRangeChange([virtualItems[0]!.index, virtualItems[virtualItems.length - 1]!.index + 1]) }, [virtualItems, onRangeChange]) React.useImperativeHandle( ref, () => ({ scrollToOffset: (px) => virtualizer.scrollToOffset(px), scrollToIndex: (index, options) => virtualizer.scrollToIndex(index, { align: options?.align ?? 'start' }), getVisibleRange: () => { const v = virtualizer.getVirtualItems() if (v.length === 0) return [0, 0] return [v[0]!.index, v[v.length - 1]!.index + 1] }, }), [virtualizer], ) const h = typeof height === 'number' ? `${height}px` : height const containerStyle: React.CSSProperties = isVertical ? { height: h, overflowY: 'auto' } : { width: h, overflowX: 'auto' } const innerStyle: React.CSSProperties = isVertical ? { height: `${totalSize}px`, position: 'relative', width: '100%' } : { width: `${totalSize}px`, position: 'relative', height: '100%' } return ( <div ref={scrollRef} data-uipkge="" data-slot="virtual-list" className={cn('w-full', className)} style={containerStyle} onScroll={onScroll} > <div style={innerStyle}> {virtualItems.map((virtualItem) => { const item = items[virtualItem.index]! const rowStyle: React.CSSProperties = isVertical ? { position: 'absolute', top: 0, left: 0, width: '100%', height: `${virtualItem.size}px`, transform: `translateY(${virtualItem.start}px)`, } : { position: 'absolute', top: 0, left: 0, height: '100%', width: `${virtualItem.size}px`, transform: `translateX(${virtualItem.start}px)`, } return ( <div key={virtualItem.key} style={rowStyle}> {children(item, virtualItem.index)} </div> ) })} </div> </div> ) } const VirtualList = React.forwardRef(VirtualListInner) as <T extends Record<string, any>>( props: VirtualListProps<T> & { ref?: React.Ref<VirtualListHandle> }, ) => React.ReactElement export { VirtualList } -
components/ui/virtual-list/index.ts 0.1 kB
export { VirtualList, type VirtualListProps, type VirtualListHandle } from './virtual-list'
Raw manifest: https://react.uipkge.dev/r/react/virtual-list.json