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,
});
OptionTypeDefault
dataTData[]required
columnsColumnDef<TData>[]required
rowHeightnumber40
overscannumber5
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 });
OptionTypeDefault
enableSortingbooleanfalse
enableMultiSortbooleanfalse
sortingSortingState
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()} />,
}
OptionTypeDefault
enableRowSelectionbooleanfalse
rowSelectionRowSelectionState
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 />}
OptionTypeDefault
enableRowExpansionbooleanfalse
expandedRowHeightnumber200
expandedExpandedState
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
OptionTypeDefault
enableKeyboardNavigationbooleanfalse
focusedRowIndexnumber
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}>
OptionTypeDefault
onScrollToBottom() => void
scrollBottomThresholdnumber100

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' }}
/>
OptionTypeDefault
enableColumnResizingbooleanfalse
columnResizeMode’onChange’ | ‘onEnd''onChange’
columnSizingColumnSizingState

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');
OptionTypeDefault
enableColumnReorderingbooleanfalse
columnOrderColumnOrderState
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 state

Controlled

You own the state:

const [sorting, setSorting] = useState([]);
useVirtualTable({
  enableSorting: true,
  sorting,
  onSortingChange: setSorting,
});