VirtualTable
A headless virtualized table hook built on TanStack Table + TanStack Virtual. Render thousands of rows with ease.
Basic Usage
ID
Name
Email
Department
Salary
1,000 rows
The simplest usage - just pass data and columns:
const { table, rows, virtualItems, totalSize, containerRef } = useVirtualTable({
data,
columns,
rowHeight: 44,
});| Option | Type | Default |
|---|---|---|
data | TData[] | required |
columns | ColumnDef<TData>[] | required |
rowHeight | number | 40 |
overscan | number | 5 |
getRowId | (row) => string | — |
Sorting
Single Column
ID
Name
Email
Department
Salary
50 rows
Multi-Column (shift+click)
ID
Name
Email
Department
Salary
50 rows • Hold Shift to multi-sort
Click headers to sort. Shift+click for multi-column.
const { sorting } = useVirtualTable({
data,
columns,
enableSorting: true,
enableMultiSort: true,
});
// Controlled:
const [sorting, setSorting] = useState([]);
useVirtualTable({ sorting, onSortingChange: setSorting });
| Option | Type | Default |
|---|---|---|
enableSorting | boolean | false |
enableMultiSort | boolean | false |
sorting | SortingState | — |
onSortingChange | (state) => void | — |
Row Selection
ID
Name
Email
Department
Salary
50 rows
Select rows with checkboxes.
const { rowSelection } = useVirtualTable({
data,
columns,
enableRowSelection: true,
getRowId: (row) => row.id,
});
// Selection column:
{
id: 'select',
header: ({ table }) => <Checkbox checked={table.getIsAllRowsSelected()} />,
cell: ({ row }) => <Checkbox checked={row.getIsSelected()} />,
}| Option | Type | Default |
|---|---|---|
enableRowSelection | boolean | false |
rowSelection | RowSelectionState | — |
onRowSelectionChange | (state) => void | — |
Row Expansion
ID
Name
Email
Department
Salary
50 rows
Expand rows to show additional content.
const { expanded } = useVirtualTable({
data,
columns,
enableRowExpansion: true,
expandedRowHeight: 100,
getRowCanExpand: (row) => true,
});
// In row render:
{row.getIsExpanded() && <ExpandedContent />}
| Option | Type | Default |
|---|---|---|
enableRowExpansion | boolean | false |
expandedRowHeight | number | 200 |
expanded | ExpandedState | — |
onExpandedChange | (state) => void | — |
Keyboard Navigation
ID
Name
Email
Department
Salary
50 rows • Use arrow keys to navigate, Space to select
Click table, then use arrow keys to navigate.
const { focusedRowIndex, handleKeyDown } = useVirtualTable({
data,
columns,
enableKeyboardNavigation: true,
});
<div ref={containerRef} tabIndex={0} onKeyDown={handleKeyDown}>↑↓ Move focus
Home End Jump to first/last
Space Toggle selection
Enter Toggle expansion
| Option | Type | Default |
|---|---|---|
enableKeyboardNavigation | boolean | false |
focusedRowIndex | number | — |
onFocusedRowChange | (index) => void | — |
Infinite Scroll
ID
Name
Email
Department
Salary
50 rows
Load more data when scrolling near the bottom.
const { handleScroll } = useVirtualTable({
data,
columns,
onScrollToBottom: () => {
if (!isFetching && hasNextPage) fetchNextPage();
},
scrollBottomThreshold: 500,
});
<div ref={containerRef} onScroll={handleScroll}>| Option | Type | Default |
|---|---|---|
onScrollToBottom | () => void | — |
scrollBottomThreshold | number | 100 |
Column Resizing
ID
Name
Email
Department
Salary
50 rows
Drag column borders to resize.
const { columnSizing } = useVirtualTable({
data,
columns,
enableColumnResizing: true,
columnResizeMode: 'onChange',
});
// Add resize handle to header:
<div
onMouseDown={header.getResizeHandler()}
style={{ cursor: 'col-resize' }}
/>| Option | Type | Default |
|---|---|---|
enableColumnResizing | boolean | false |
columnResizeMode | ’onChange’ | ‘onEnd' | 'onChange’ |
columnSizing | ColumnSizingState | — |
Column Reordering
ID
Name
Email
Department
Salary
50 rows
Drag and drop columns to reorder.
const { columnOrder, reorderColumn } = useVirtualTable({
data,
columns,
enableColumnReordering: true,
});
reorderColumn('sourceId', 'targetId');
| Option | Type | Default |
|---|---|---|
enableColumnReordering | boolean | false |
columnOrder | ColumnOrderState | — |
onColumnOrderChange | (state) => void | — |
Return Values
const {
// Core
table, // TanStack Table instance
rows, // Processed row models
virtualizer, // TanStack Virtual instance
virtualItems, // Currently visible virtual items
totalSize, // Total scrollable height (px)
containerRef, // Ref for scroll container
// Handlers
handleScroll, // For infinite scroll
handleKeyDown, // For keyboard navigation
reorderColumn, // (fromId, toId) => void
setFocusedRow, // (index) => void
// State (controlled or internal)
rowSelection, sorting, expanded, columnSizing, columnOrder, focusedRowIndex,
} = useVirtualTable(options);
Controlled vs Uncontrolled
Uncontrolled
State managed internally:
const { sorting } = useVirtualTable({
enableSorting: true,
});
// `sorting` reflects internal stateControlled
You own the state:
const [sorting, setSorting] = useState([]);
useVirtualTable({
enableSorting: true,
sorting,
onSortingChange: setSorting,
});