A virtual scroll React component for efficiently rendering large scrollable lists, grids, tables, and feeds
—
Specialized virtualization components for HTML tables that maintain proper table semantics while efficiently rendering large datasets. Supports fixed headers and footers, grouped tables, and all standard table features with virtualized row rendering.
Virtualized HTML table component that renders only visible rows while maintaining proper table structure and semantics.
/**
* Virtualized HTML table component with fixed headers and footers
* @param props - Configuration options for the virtualized table
* @returns JSX.Element representing the virtualized table
*/
function TableVirtuoso<D = any, C = any>(props: TableVirtuosoProps<D, C>): JSX.Element;
interface TableVirtuosoProps<D, C> extends Omit<VirtuosoProps<D, C>, 'components' | 'headerFooterTag'> {
/** The data items to be rendered. If data is set, totalCount will be inferred from the length */
data?: readonly D[];
/** The total amount of items to be rendered */
totalCount?: number;
/** Set the callback to specify the contents of each table row */
itemContent?: ItemContent<D, C>;
/** Use the components property for advanced customization of table elements */
components?: TableComponents<D, C>;
/** Additional context available in custom components and content callbacks */
context?: C;
/** If specified, the component will use the function to generate the key property for each row */
computeItemKey?: ComputeItemKey<D, C>;
/** Set the contents of the table header */
fixedHeaderContent?: FixedHeaderContent;
/** Set the contents of the table footer */
fixedFooterContent?: FixedFooterContent;
/** Setting alignToBottom to true aligns the items to the bottom if shorter than viewport */
alignToBottom?: boolean;
/** Called with true / false when the table has reached the bottom / gets scrolled up */
atBottomStateChange?: (atBottom: boolean) => void;
/** Called with true / false when the table has reached the top / gets scrolled down */
atTopStateChange?: (atTop: boolean) => void;
/** By default 4. Redefine to change how much away from the bottom the scroller can be */
atBottomThreshold?: number;
/** By default 0. Redefine to change how much away from the top the scroller can be */
atTopThreshold?: number;
/** Pass a reference to a scrollable parent element */
customScrollParent?: HTMLElement;
/** By default, the component assumes the default item height from the first rendered item */
defaultItemHeight?: number;
/** Gets called when the user scrolls to the end of the table */
endReached?: (index: number) => void;
/** Use when implementing inverse infinite scrolling */
firstItemIndex?: number;
/** Can be used to improve performance if the rendered items are of known size */
fixedItemHeight?: number;
/** If set to true, the table automatically scrolls to bottom if the total count is changed */
followOutput?: FollowOutput;
/** Set the increaseViewportBy property to artificially increase the viewport size */
increaseViewportBy?: number | { top: number; bottom: number };
/** Use for server-side rendering */
initialItemCount?: number;
/** Set this value to offset the initial location of the table */
initialScrollTop?: number;
/** Set to a value between 0 and totalCount - 1 to make the table start scrolled to that item */
initialTopMostItemIndex?: IndexLocationWithAlign | number;
/** Called when the table starts/stops scrolling */
isScrolling?: (isScrolling: boolean) => void;
/** Allows customizing the height/width calculation of row elements */
itemSize?: SizeFunction;
/** Called with the new set of items each time the table rows are rendered due to scrolling */
itemsRendered?: (items: ListItem<D>[]) => void;
/** Set the overscan property to make the component chunk the rendering of new items on scroll */
overscan?: number | { main: number; reverse: number };
/** Called with the new set of items each time the table rows are rendered due to scrolling */
rangeChanged?: (range: ListRange) => void;
/** Pass a state obtained from getState() method to restore the table state */
restoreStateFrom?: StateSnapshot;
/** Provides access to the root DOM element */
scrollerRef?: (ref: HTMLElement | null | Window) => any;
/** Use to display placeholders if the user scrolls fast through the table */
scrollSeekConfiguration?: false | ScrollSeekConfiguration;
/** Called when the user scrolls to the start of the table */
startReached?: (index: number) => void;
/** Set the amount of items to remain fixed at the top of the table */
topItemCount?: number;
/** Called when the total table height is changed due to new items or viewport resize */
totalListHeightChanged?: (height: number) => void;
/** Uses the document scroller rather than wrapping the table in its own */
useWindowScroll?: boolean;
}
type FixedHeaderContent = (() => React.ReactNode) | null;
type FixedFooterContent = (() => React.ReactNode) | null;Usage Examples:
import React from 'react';
import { TableVirtuoso } from 'react-virtuoso';
// Basic data table
function UserTable() {
const users = Array.from({ length: 10000 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
age: Math.floor(Math.random() * 50) + 18,
city: ['New York', 'London', 'Tokyo', 'Paris'][Math.floor(Math.random() * 4)]
}));
return (
<TableVirtuoso
style={{ height: '500px' }}
data={users}
fixedHeaderContent={() => (
<tr style={{ backgroundColor: '#f5f5f5' }}>
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>ID</th>
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Name</th>
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Email</th>
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Age</th>
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>City</th>
</tr>
)}
itemContent={(index, user) => (
<>
<td style={{ padding: '12px', borderBottom: '1px solid #eee' }}>{user.id}</td>
<td style={{ padding: '12px', borderBottom: '1px solid #eee' }}>{user.name}</td>
<td style={{ padding: '12px', borderBottom: '1px solid #eee' }}>{user.email}</td>
<td style={{ padding: '12px', borderBottom: '1px solid #eee' }}>{user.age}</td>
<td style={{ padding: '12px', borderBottom: '1px solid #eee' }}>{user.city}</td>
</>
)}
/>
);
}
// Table with footer
function SalesTable() {
const sales = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
product: `Product ${i + 1}`,
amount: Math.floor(Math.random() * 1000) + 10,
date: new Date(2023, Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1)
}));
const totalAmount = sales.reduce((sum, sale) => sum + sale.amount, 0);
return (
<TableVirtuoso
style={{ height: '600px' }}
data={sales}
fixedHeaderContent={() => (
<tr style={{ backgroundColor: '#e3f2fd' }}>
<th style={{ padding: '12px', textAlign: 'left' }}>Product</th>
<th style={{ padding: '12px', textAlign: 'right' }}>Amount</th>
<th style={{ padding: '12px', textAlign: 'left' }}>Date</th>
</tr>
)}
fixedFooterContent={() => (
<tr style={{ backgroundColor: '#f5f5f5', fontWeight: 'bold' }}>
<td style={{ padding: '12px' }}>Total</td>
<td style={{ padding: '12px', textAlign: 'right' }}>${totalAmount.toLocaleString()}</td>
<td style={{ padding: '12px' }}></td>
</tr>
)}
itemContent={(index, sale) => (
<>
<td style={{ padding: '12px', borderBottom: '1px solid #eee' }}>
{sale.product}
</td>
<td style={{ padding: '12px', textAlign: 'right', borderBottom: '1px solid #eee' }}>
${sale.amount.toLocaleString()}
</td>
<td style={{ padding: '12px', borderBottom: '1px solid #eee' }}>
{sale.date.toLocaleDateString()}
</td>
</>
)}
/>
);
}
// Interactive table with sorting
function InteractiveTable() {
const [sortField, setSortField] = React.useState<string>('name');
const [sortDirection, setSortDirection] = React.useState<'asc' | 'desc'>('asc');
const rawData = Array.from({ length: 5000 }, (_, i) => ({
id: i + 1,
name: `Item ${i + 1}`,
value: Math.floor(Math.random() * 1000),
category: ['A', 'B', 'C', 'D'][Math.floor(Math.random() * 4)]
}));
const sortedData = React.useMemo(() => {
return [...rawData].sort((a, b) => {
const aVal = a[sortField as keyof typeof a];
const bVal = b[sortField as keyof typeof b];
const modifier = sortDirection === 'asc' ? 1 : -1;
return aVal < bVal ? -modifier : aVal > bVal ? modifier : 0;
});
}, [rawData, sortField, sortDirection]);
const handleSort = (field: string) => {
if (sortField === field) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection('asc');
}
};
const SortableHeader: React.FC<{ field: string; children: React.ReactNode }> = ({ field, children }) => (
<th
style={{
padding: '12px',
cursor: 'pointer',
backgroundColor: sortField === field ? '#e3f2fd' : '#f5f5f5',
userSelect: 'none'
}}
onClick={() => handleSort(field)}
>
{children}
{sortField === field && (sortDirection === 'asc' ? ' ↑' : ' ↓')}
</th>
);
return (
<TableVirtuoso
style={{ height: '500px' }}
data={sortedData}
fixedHeaderContent={() => (
<tr>
<SortableHeader field="id">ID</SortableHeader>
<SortableHeader field="name">Name</SortableHeader>
<SortableHeader field="value">Value</SortableHeader>
<SortableHeader field="category">Category</SortableHeader>
</tr>
)}
itemContent={(index, item) => (
<>
<td style={{ padding: '12px', borderBottom: '1px solid #eee' }}>{item.id}</td>
<td style={{ padding: '12px', borderBottom: '1px solid #eee' }}>{item.name}</td>
<td style={{ padding: '12px', borderBottom: '1px solid #eee' }}>{item.value}</td>
<td style={{ padding: '12px', borderBottom: '1px solid #eee' }}>{item.category}</td>
</>
)}
/>
);
}
// Table with custom components
function CustomTable() {
const data = Array.from({ length: 1000 }, (_, i) => ({
id: i + 1,
name: `Product ${i + 1}`,
status: ['Active', 'Inactive', 'Pending'][Math.floor(Math.random() * 3)]
}));
return (
<TableVirtuoso
style={{ height: '400px' }}
data={data}
components={{
Table: (props) => (
<table {...props} style={{ ...props.style, borderCollapse: 'collapse', width: '100%' }} />
),
TableRow: ({ item, ...props }) => (
<tr
{...props}
style={{
...props.style,
backgroundColor: item.status === 'Active' ? '#e8f5e8' :
item.status === 'Inactive' ? '#ffe8e8' : '#fff8e1'
}}
/>
)
}}
fixedHeaderContent={() => (
<tr>
<th style={{ padding: '12px', border: '1px solid #ddd' }}>ID</th>
<th style={{ padding: '12px', border: '1px solid #ddd' }}>Name</th>
<th style={{ padding: '12px', border: '1px solid #ddd' }}>Status</th>
</tr>
)}
itemContent={(index, item) => (
<>
<td style={{ padding: '12px', border: '1px solid #ddd' }}>{item.id}</td>
<td style={{ padding: '12px', border: '1px solid #ddd' }}>{item.name}</td>
<td style={{ padding: '12px', border: '1px solid #ddd' }}>
<span style={{
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px',
fontWeight: 'bold',
color: item.status === 'Active' ? '#2e7d32' :
item.status === 'Inactive' ? '#d32f2f' : '#f57c00'
}}>
{item.status}
</span>
</td>
</>
)}
/>
);
}Table virtualization with grouped rows and sticky group headers, perfect for categorized tabular data.
/**
* Virtualized HTML table component with grouped rows and sticky headers
* @param props - Configuration options for the grouped virtualized table
* @returns JSX.Element representing the grouped virtualized table
*/
function GroupedTableVirtuoso<D = any, C = any>(props: GroupedTableVirtuosoProps<D, C>): JSX.Element;
interface GroupedTableVirtuosoProps<D, C> extends Omit<TableVirtuosoProps<D, C>, 'itemContent' | 'totalCount'> {
/** Specifies the amount of items in each group */
groupCounts?: number[];
/** Specifies how each group header gets rendered */
groupContent?: GroupContent<C>;
/** Specifies how each item gets rendered with group context */
itemContent?: GroupItemContent<D, C>;
/** Use when implementing inverse infinite scrolling */
firstItemIndex?: number;
}
type GroupContent<C> = (index: number, context: C) => React.ReactNode;
type GroupItemContent<D, C> = (index: number, groupIndex: number, data: D, context: C) => React.ReactNode;Usage Example:
import React from 'react';
import { GroupedTableVirtuoso } from 'react-virtuoso';
function GroupedSalesTable() {
const salesByRegion = [
{
region: 'North America',
sales: Array.from({ length: 100 }, (_, i) => ({
id: i + 1,
product: `Product ${i + 1}`,
revenue: Math.floor(Math.random() * 10000) + 1000
}))
},
{
region: 'Europe',
sales: Array.from({ length: 150 }, (_, i) => ({
id: i + 101,
product: `Product ${i + 101}`,
revenue: Math.floor(Math.random() * 10000) + 1000
}))
},
{
region: 'Asia Pacific',
sales: Array.from({ length: 200 }, (_, i) => ({
id: i + 251,
product: `Product ${i + 251}`,
revenue: Math.floor(Math.random() * 10000) + 1000
}))
}
];
const groupCounts = salesByRegion.map(region => region.sales.length);
const allSales = salesByRegion.flatMap(region =>
region.sales.map(sale => ({ ...sale, region: region.region }))
);
return (
<GroupedTableVirtuoso
style={{ height: '600px' }}
groupCounts={groupCounts}
fixedHeaderContent={() => (
<tr style={{ backgroundColor: '#f0f0f0' }}>
<th style={{ padding: '12px', textAlign: 'left' }}>Product</th>
<th style={{ padding: '12px', textAlign: 'right' }}>Revenue</th>
</tr>
)}
groupContent={(index) => (
<tr style={{ backgroundColor: '#e3f2fd' }}>
<td colSpan={2} style={{
padding: '16px',
fontWeight: 'bold',
fontSize: '16px',
borderBottom: '2px solid #2196f3'
}}>
📍 {salesByRegion[index].region}
</td>
</tr>
)}
itemContent={(index, groupIndex, sale) => (
<>
<td style={{ padding: '12px', paddingLeft: '24px', borderBottom: '1px solid #eee' }}>
{sale.product}
</td>
<td style={{ padding: '12px', textAlign: 'right', borderBottom: '1px solid #eee' }}>
${sale.revenue.toLocaleString()}
</td>
</>
)}
data={allSales}
/>
);
}Customize table elements through the components interface for full control over table rendering.
interface TableComponents<Data = any, Context = any> {
/** Set to render a custom UI when the table is empty */
EmptyPlaceholder?: React.ComponentType<ContextProp<Context>>;
/** Set to render an empty item placeholder */
FillerRow?: React.ComponentType<FillerRowProps & ContextProp<Context>>;
/** Set to customize the outermost scrollable element */
Scroller?: React.ComponentType<ScrollerProps & ContextProp<Context>>;
/** Set to render an item placeholder when the user scrolls fast */
ScrollSeekPlaceholder?: React.ComponentType<ScrollSeekPlaceholderProps & ContextProp<Context>>;
/** Set to customize the wrapping table element */
Table?: React.ComponentType<TableProps & ContextProp<Context>>;
/** Set to customize the items wrapper. Default is tbody */
TableBody?: React.ComponentType<TableBodyProps & ContextProp<Context>>;
/** Set to customize the group item wrapping element */
Group?: React.ComponentType<GroupProps & ContextProp<Context>>;
/** Set to render a fixed footer at the bottom of the table (tfoot) */
TableFoot?: React.ComponentType<
Pick<React.ComponentProps<'tfoot'>, 'children' | 'style'> &
React.RefAttributes<HTMLTableSectionElement> &
ContextProp<Context>
>;
/** Set to render a fixed header at the top of the table (thead) */
TableHead?: React.ComponentType<
Pick<React.ComponentProps<'thead'>, 'children' | 'style'> &
React.RefAttributes<HTMLTableSectionElement> &
ContextProp<Context>
>;
/** Set to customize the item wrapping element. Default is tr */
TableRow?: React.ComponentType<ItemProps<Data> & ContextProp<Context>>;
}
interface FillerRowProps {
height: number;
}
type TableProps = Pick<React.ComponentProps<'table'>, 'children' | 'style'>;
type TableBodyProps = Pick<React.ComponentProps<'tbody'>, 'children' | 'className' | 'style'> &
React.RefAttributes<HTMLTableSectionElement> & {
'data-testid': string;
};
type TableRootProps = Omit<React.HTMLProps<HTMLTableElement>, 'data' | 'ref'>;Programmatic control interface for table scrolling and state management.
interface TableVirtuosoHandle {
/** Obtains the internal size state of the component for restoration */
getState(stateCb: StateCallback): void;
/** Scrolls the component with the specified amount */
scrollBy(location: ScrollToOptions): void;
/** Scrolls the component to the specified location */
scrollTo(location: ScrollToOptions): void;
/** Scrolls the item into view if necessary */
scrollIntoView(location: FlatScrollIntoViewLocation | number): void;
/** Scrolls the component to the specified row index */
scrollToIndex(location: FlatIndexLocationWithAlign | number): void;
}
interface GroupedTableVirtuosoHandle {
/** Obtains the internal size state of the component for restoration */
getState(stateCb: StateCallback): void;
/** Scrolls the component with the specified amount */
scrollBy(location: ScrollToOptions): void;
/** Scrolls the component to the specified location */
scrollTo(location: ScrollToOptions): void;
/** Scrolls the item into view if necessary */
scrollIntoView(location: ScrollIntoViewLocationOptions): void;
/** Scrolls the component to the specified item index */
scrollToIndex(location: IndexLocationWithAlign | number): void;
}Usage Example:
function ControlledTable() {
const tableRef = React.useRef<TableVirtuosoHandle>(null);
const scrollToTop = () => {
tableRef.current?.scrollToIndex(0);
};
const scrollToRow = (index: number) => {
tableRef.current?.scrollToIndex({
index,
align: 'center',
behavior: 'smooth'
});
};
return (
<div>
<div>
<button onClick={scrollToTop}>Scroll to Top</button>
<button onClick={() => scrollToRow(500)}>Scroll to Row 500</button>
</div>
<TableVirtuoso
ref={tableRef}
data={data}
fixedHeaderContent={() => <tr><th>Data</th></tr>}
itemContent={(index, item) => <td>{item}</td>}
/>
</div>
);
}interface ScrollIntoViewLocationOptions {
align?: 'center' | 'end' | 'start';
behavior?: 'auto' | 'smooth';
calculateViewLocation?: CalculateViewLocation;
done?: () => void;
}
interface FlatScrollIntoViewLocation extends ScrollIntoViewLocationOptions {
index: number;
}
interface StateSnapshot {
ranges: SizeRange[];
scrollTop: number;
}
interface SizeRange {
startIndex: number;
endIndex: number;
size: number;
}
type StateCallback = (state: StateSnapshot) => void;
type SizeFunction = (el: HTMLElement, field: 'offsetHeight' | 'offsetWidth') => number;Install with Tessl CLI
npx tessl i tessl/npm-react-virtuoso