Virtual List A performant virtualized list that only renders visible items, supporting thousands of rows with smooth scrolling.
A performant virtualized list that only renders visible items in the DOM, enabling smooth scrolling through thousands of rows without impacting performance.
<VirtualList items={users} // 1,000 items itemHeight={56} height={350} renderItem={(user, index) => ( <div className="flex w-full items-center justify-between gap-3 px-3"> <div className="flex min-w-0 items-center gap-3"> <Avatar src={user.avatar} size="sm" /> <div className="min-w-0"> <p className="truncate text-sm font-medium">{user.name}</p> <p className="truncate text-xs text-muted-foreground">{user.email}</p> </div> </div> <Badge variant="secondary" size="sm">{user.role}</Badge> </div> )} />
Install the component via the CLI in one command.
npm pnpm yarn bun
npx @work-rjkashyap/unified-ui add virtual-list pnpm dlx @work-rjkashyap/unified-ui add virtual-list npx @work-rjkashyap/unified-ui add virtual-list bunx @work-rjkashyap/unified-ui add virtual-list
If you haven't initialized your project yet, run npx @work-rjkashyap/unified-ui init first. See the CLI docs for
details.
Use this approach if you prefer to install the entire design system as a dependency instead of copying individual components.
npm pnpm yarn bun
npm install @work-rjkashyap/unified-ui pnpm add @work-rjkashyap/unified-ui yarn add @work-rjkashyap/unified-ui bun add @work-rjkashyap/unified-ui
import { VirtualList } from "@work-rjkashyap/unified-ui" ;
<VirtualList items={users} // 1,000 items itemHeight={48} height={300} renderItem={(user, index) => ( <div className="flex w-full items-center gap-3 px-3"> <span className="truncate text-sm font-medium">{user.name}</span> </div> )} />
<VirtualList items={users} itemHeight={64} height={320} renderItem={(user) => ( <div className="flex w-full items-center justify-between gap-3 px-3"> <div className="flex min-w-0 items-center gap-3"> <Avatar src={user.avatar} size="sm" /> <div className="min-w-0"> <p className="truncate text-sm font-medium">{user.name}</p> <p className="truncate text-xs text-muted-foreground">{user.email}</p> </div> </div> <Badge variant={user.role === "Admin" ? "primary" : "secondary"} size="sm"> {user.role} </Badge> </div> )} />
{/* Minimal overscan — fewer DOM nodes */} <VirtualList items={items} itemHeight={40} height={200} overscan={2} renderItem={(item, index) => ( <div className="flex w-full items-center gap-3 px-3"> <span className="w-6 shrink-0 text-right text-xs tabular-nums text-muted-foreground"> {index + 1} </span> <span className="truncate text-sm">{item.name}</span> </div> )} /> {/* Higher overscan — smoother fast scrolling */} <VirtualList items={items} itemHeight={40} height={200} overscan={10} renderItem={(item, index) => ( <div className="flex w-full items-center gap-3 px-3"> <span className="w-6 shrink-0 text-right text-xs tabular-nums text-muted-foreground"> {index + 1} </span> <span className="truncate text-sm">{item.name}</span> </div> )} />
<VirtualList items={users} itemHeight={44} height={250} getItemKey={(user) => user.id} renderItem={(user) => ( <div className="flex w-full items-center justify-between px-3"> <span className="truncate text-sm">{user.name}</span> <span className="shrink-0 rounded bg-muted px-1.5 py-0.5 text-xs tabular-nums text-muted-foreground"> ID: {user.id} </span> </div> )} />
50 items loaded — scroll to the bottom to load more const [items, setItems] = useState(initialItems); const [loading, setLoading] = useState(false); const loadMore = async () => { setLoading(true); const newItems = await fetchMore(items.length); setItems(prev => [...prev, ...newItems]); setLoading(false); }; <VirtualList items={items} itemHeight={40} height={300} loading={loading} onEndReached={loadMore} endReachedThreshold={200} renderItem={(item) => ( <div className="flex w-full items-center gap-3 px-3"> <span className="truncate text-sm">{item.name}</span> </div> )} />
<VirtualList items={items} itemHeight={44} height={250} loading={true} renderItem={(item) => ( <div className="flex w-full items-center gap-3 px-3"> <span className="truncate text-sm">{item.name}</span> </div> )} />
<VirtualList items={items} itemHeight={44} height={250} loading={true} loadingIndicator={ <div className="flex items-center justify-center gap-2 py-3"> <Spinner size="sm" /> <span className="text-sm font-medium text-muted-foreground">Loading more users...</span> </div> } renderItem={(item) => ( <div className="flex w-full items-center gap-3 px-3"> <span className="truncate text-sm">{item.name}</span> </div> )} />
No users found Try adjusting your search or filters. <VirtualList items={[]} itemHeight={48} height={250} emptyContent={ <div className="flex flex-col items-center gap-1.5 text-center"> <p className="text-sm font-semibold text-foreground">No users found</p> <p className="text-xs text-muted-foreground"> Try adjusting your search or filters. </p> </div> } renderItem={() => null} />
{/* Compact */} <VirtualList items={items} itemHeight={36} height={150} renderItem={(item) => ( <div className="flex w-full items-center px-3"> <span className="truncate text-sm">{item.name}</span> </div> )} /> {/* Taller */} <VirtualList items={items} itemHeight={36} height={300} renderItem={(item) => ( <div className="flex w-full items-center px-3"> <span className="truncate text-sm">{item.name}</span> </div> )} />
Prop Type Default Description itemsT[]— Array of items to render. itemHeightnumber— Height of each item in pixels. renderItem(item: T, index: number) => ReactNode— Render function for each item. heightnumber400Height of the scrollable container. overscannumber5Extra items to render above/below viewport. getItemKey(item: T, index: number) => string | number— Custom key extractor. onEndReached() => void— Callback when scroll reaches the bottom. endReachedThresholdnumber100Distance from bottom (px) to trigger end reached. loadingbooleanfalseShows a loading indicator at the bottom. loadingIndicatorReactNode— Custom loading indicator. emptyContentReactNode— Content when items array is empty. classNamestring— Additional CSS classes on the container. itemClassNamestring— Additional CSS classes on each item wrapper.
The total scroll height is calculated as items.length × itemHeight.
On scroll, the component calculates which items are in the visible window .
Only the visible items ± the overscan buffer are rendered to the DOM.
Items are absolutely positioned within a container matching the total height, creating a native scrollbar.
When onEndReached is provided, it fires when the scroll distance from the bottom is less than endReachedThreshold.
Uses role="list" on the container and role="listitem" on each item.
Each item includes aria-setsize (total count) and aria-posinset (position) for screen readers.
Keyboard scrolling works via native scroll behavior.
Empty state container maintains the same role and dimensions.
For variable-height items or bidirectional scrolling, consider using
@tanstack/react-virtual directly. This component is optimized for
fixed-height items only.
Keep renderItem lightweight — avoid expensive computations inside the render function.
Memoize items — if your items array is recreated on every render, wrap it with useMemo.
Use getItemKey — stable keys prevent unnecessary re-renders when items change.
Pair with InfiniteScroll — for paginated data, use onEndReached to fetch the next page.
Token Usage --backgroundContainer background --borderContainer and item borders --radius-lgContainer border radius --primaryLoading spinner accent --muted-foregroundEmpty state text