diff --git a/docs/config.json b/docs/config.json
index e0da28e51b..0b68b8d81e 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -602,10 +602,18 @@
"to": "framework/react/examples/virtualized-columns",
"label": "Virtualized Columns"
},
+ {
+ "to": "framework/react/examples/virtualized-columns-experimental",
+ "label": "Virtualized Columns (Experimental)"
+ },
{
"to": "framework/react/examples/virtualized-rows",
"label": "Virtualized Rows"
},
+ {
+ "to": "framework/react/examples/virtualized-rows-experimental",
+ "label": "Virtualized Rows (Experimental)"
+ },
{
"to": "framework/react/examples/virtualized-infinite-scrolling",
"label": "Virtualized Infinite Scrolling"
diff --git a/examples/react/virtualized-columns-expiremental/.gitignore b/examples/react/virtualized-columns-expiremental/.gitignore
new file mode 100644
index 0000000000..d451ff16c1
--- /dev/null
+++ b/examples/react/virtualized-columns-expiremental/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
diff --git a/examples/react/virtualized-columns-expiremental/README.md b/examples/react/virtualized-columns-expiremental/README.md
new file mode 100644
index 0000000000..b168d3c4b1
--- /dev/null
+++ b/examples/react/virtualized-columns-expiremental/README.md
@@ -0,0 +1,6 @@
+# Example
+
+To run this example:
+
+- `npm install` or `yarn`
+- `npm run start` or `yarn start`
diff --git a/examples/react/virtualized-columns-expiremental/index.html b/examples/react/virtualized-columns-expiremental/index.html
new file mode 100644
index 0000000000..fa04f89341
--- /dev/null
+++ b/examples/react/virtualized-columns-expiremental/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
+
+
diff --git a/examples/react/virtualized-columns-expiremental/package.json b/examples/react/virtualized-columns-expiremental/package.json
new file mode 100644
index 0000000000..c9d262a0ca
--- /dev/null
+++ b/examples/react/virtualized-columns-expiremental/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "tanstack-table-example-virtualized-columns-experimental",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "serve": "vite preview",
+ "start": "vite"
+ },
+ "dependencies": {
+ "@faker-js/faker": "^8.4.1",
+ "@tanstack/react-table": "^8.20.6",
+ "@tanstack/react-virtual": "^3.12.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@rollup/plugin-replace": "^5.0.7",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.1",
+ "typescript": "5.4.5",
+ "vite": "^5.3.2"
+ }
+}
diff --git a/examples/react/virtualized-columns-expiremental/src/index.css b/examples/react/virtualized-columns-expiremental/src/index.css
new file mode 100644
index 0000000000..98d667b225
--- /dev/null
+++ b/examples/react/virtualized-columns-expiremental/src/index.css
@@ -0,0 +1,53 @@
+:root {
+ --virtual-padding-left: 0px;
+ --virtual-padding-right: 0px;
+}
+
+html {
+ font-family: sans-serif;
+ font-size: 14px;
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+ font-family: arial, sans-serif;
+ table-layout: fixed;
+}
+
+thead {
+ background: lightgray;
+}
+
+tr {
+ border-bottom: 1px solid lightgray;
+}
+
+th {
+ border-bottom: 1px solid lightgray;
+ border-right: 1px solid lightgray;
+ padding: 2px 4px;
+ text-align: left;
+}
+
+td {
+ padding: 6px;
+}
+
+.container {
+ border: 1px solid lightgray;
+ margin: 1rem auto;
+}
+
+.app {
+ margin: 1rem auto;
+ text-align: center;
+}
+
+.left-column-spacer {
+ width: var(--virtual-padding-left);
+}
+
+.right-column-spacer {
+ width: var(--virtual-padding-right);
+}
diff --git a/examples/react/virtualized-columns-expiremental/src/main.tsx b/examples/react/virtualized-columns-expiremental/src/main.tsx
new file mode 100644
index 0000000000..b148f30f5a
--- /dev/null
+++ b/examples/react/virtualized-columns-expiremental/src/main.tsx
@@ -0,0 +1,370 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import './index.css'
+import {
+ Cell,
+ ColumnDef,
+ Header,
+ HeaderGroup,
+ Row,
+ Table,
+ flexRender,
+ getCoreRowModel,
+ getSortedRowModel,
+ useReactTable,
+} from '@tanstack/react-table'
+import { useVirtualizer, Virtualizer } from '@tanstack/react-virtual'
+import { makeColumns, makeData, Person } from './makeData'
+
+// All important CSS styles are included as inline styles for this example. This is not recommended for your code.
+function App() {
+ const columns = React.useMemo[]>(
+ () => makeColumns(1_000),
+ []
+ )
+
+ const [data, setData] = React.useState(() => makeData(1_000, columns))
+
+ const refreshData = React.useCallback(() => {
+ setData(makeData(1_000, columns))
+ }, [columns])
+
+ const table = useReactTable({
+ data,
+ columns,
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ debugTable: true,
+ })
+
+ return (
+
+ {process.env.NODE_ENV === 'development' ? (
+
+ Notice: You are currently running React in
+ development mode. Virtualized rendering performance will be slightly
+ degraded until this application is built for production.
+
+ ) : null}
+
({columns.length.toLocaleString()} columns)
+
({data.length.toLocaleString()} rows)
+
Refresh Data
+
+
+ )
+}
+
+interface TableContainerProps {
+ table: Table
+}
+
+function TableContainer({ table }: TableContainerProps) {
+ const visibleColumns = table.getVisibleLeafColumns()
+
+ //The virtualizers need to know the scrollable container element
+ const tableContainerRef = React.useRef(null)
+
+ //we are using a slightly different virtualization strategy for columns (compared to virtual rows) in order to support dynamic row heights
+ const columnVirtualizer = useVirtualizer<
+ HTMLDivElement,
+ HTMLTableCellElement
+ >({
+ count: visibleColumns.length,
+ estimateSize: index => visibleColumns[index].getSize(), //estimate width of each column for accurate scrollbar dragging
+ getScrollElement: () => tableContainerRef.current,
+ horizontal: true,
+ overscan: 3, //how many columns to render on each side off screen each way (adjust this for performance)
+ onChange: instance => {
+ const virtualColumns = instance.getVirtualItems()
+ // different virtualization strategy for columns - instead of absolute and translateY, we add empty columns to the left and right
+ const virtualPaddingLeft = virtualColumns[0]?.start ?? 0
+ const virtualPaddingRight =
+ instance.getTotalSize() -
+ (virtualColumns[virtualColumns.length - 1]?.end ?? 0)
+
+ document.documentElement.style.setProperty(
+ '--virtual-padding-left',
+ `${virtualPaddingLeft}px`
+ )
+ document.documentElement.style.setProperty(
+ '--virtual-padding-right',
+ `${virtualPaddingRight}px`
+ )
+ },
+ })
+
+ return (
+
+ {/* Even though we're still using sematic table tags, we must use CSS grid and flexbox for dynamic row heights */}
+
+
+ )
+}
+
+interface TableHeadProps {
+ columnVirtualizer: Virtualizer
+ table: Table
+}
+
+function TableHead({ table, columnVirtualizer }: TableHeadProps) {
+ return (
+
+ {table.getHeaderGroups().map(headerGroup => (
+
+ ))}
+
+ )
+}
+
+interface TableHeadRowProps {
+ columnVirtualizer: Virtualizer
+ headerGroup: HeaderGroup
+}
+
+function TableHeadRow({ columnVirtualizer, headerGroup }: TableHeadRowProps) {
+ const virtualColumnIndexes = columnVirtualizer.getVirtualIndexes()
+
+ return (
+
+ {/* fake empty column to the left for virtualization scroll padding */}
+
+ {virtualColumnIndexes.map(virtualColumnIndex => {
+ const header = headerGroup.headers[virtualColumnIndex]
+ return (
+
+ )
+ })}
+ {/* fake empty column to the right for virtualization scroll padding */}
+
+
+ )
+}
+
+interface TableHeadCellProps {
+ columnVirtualizer: Virtualizer
+ header: Header
+}
+
+function TableHeadCell({
+ columnVirtualizer: _columnVirtualizer,
+ header,
+}: TableHeadCellProps) {
+ return (
+
+
+ {flexRender(header.column.columnDef.header, header.getContext())}
+ {{
+ asc: ' 🔼',
+ desc: ' 🔽',
+ }[header.column.getIsSorted() as string] ?? null}
+
+
+ )
+}
+
+const TableHeadCellMemo = React.memo(
+ TableHeadCell,
+ (_prev, next) => next.columnVirtualizer.isScrolling
+) as typeof TableHeadCell
+
+interface TableBodyProps {
+ columnVirtualizer: Virtualizer
+ table: Table
+ tableContainerRef: React.RefObject
+}
+
+function TableBody({
+ columnVirtualizer,
+ table,
+ tableContainerRef,
+}: TableBodyProps) {
+ const rowRefsMap = React.useRef>(new Map())
+
+ const { rows } = table.getRowModel()
+
+ //dynamic row height virtualization - alternatively you could use a simpler fixed row height strategy without the need for `measureElement`
+ const rowVirtualizer = useVirtualizer({
+ count: rows.length,
+ estimateSize: () => 33, //estimate row height for accurate scrollbar dragging
+ getScrollElement: () => tableContainerRef.current,
+ //measure dynamic row height, except in firefox because it measures table border height incorrectly
+ measureElement:
+ typeof window !== 'undefined' &&
+ navigator.userAgent.indexOf('Firefox') === -1
+ ? element => element?.getBoundingClientRect().height
+ : undefined,
+ overscan: 5,
+ onChange: instance => {
+ instance.getVirtualItems().forEach(virtualRow => {
+ const rowRef = rowRefsMap.current.get(virtualRow.index)
+ if (!rowRef) return
+ rowRef.style.transform = `translateY(${virtualRow.start}px)`
+ })
+ },
+ })
+
+ const virtualRowIndexes = rowVirtualizer.getVirtualIndexes()
+
+ return (
+
+ {virtualRowIndexes.map(virtualRowIndex => {
+ const row = rows[virtualRowIndex] as Row
+
+ return (
+
+ )
+ })}
+
+ )
+}
+
+interface TableBodyRowProps {
+ columnVirtualizer: Virtualizer
+ row: Row
+ rowVirtualizer: Virtualizer
+ virtualRowIndex: number
+ rowRefsMap: React.MutableRefObject>
+}
+
+function TableBodyRow({
+ columnVirtualizer,
+ row,
+ rowVirtualizer,
+ virtualRowIndex,
+ rowRefsMap,
+}: TableBodyRowProps) {
+ const visibleCells = row.getVisibleCells()
+ const virtualColumnIndexes = columnVirtualizer.getVirtualIndexes()
+
+ return (
+ {
+ if (node) {
+ rowVirtualizer.measureElement(node)
+ rowRefsMap.current.set(virtualRowIndex, node)
+ }
+ }} //measure dynamic row height
+ key={row.id}
+ style={{
+ display: 'flex',
+ position: 'absolute',
+ width: '100%',
+ }}
+ >
+ {/* fake empty column to the left for virtualization scroll padding */}
+
+ {virtualColumnIndexes.map(virtualColumnIndex => {
+ const cell = visibleCells[virtualColumnIndex]
+ return (
+
+ )
+ })}
+ {/* fake empty column to the right for virtualization scroll padding */}
+
+
+ )
+}
+
+// TODO: Can rows be memoized in any way without breaking column virtualization?
+// const TableBodyRowMemo = React.memo(
+// TableBodyRow,
+// (_prev, next) => next.rowVirtualizer.isScrolling
+// )
+
+interface TableBodyCellProps {
+ cell: Cell
+ columnVirtualizer: Virtualizer
+}
+
+function TableBodyCell({
+ cell,
+ columnVirtualizer: _columnVirtualizer,
+}: TableBodyCellProps) {
+ return (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ )
+}
+
+const TableBodyCellMemo = React.memo(
+ TableBodyCell,
+ (_prev, next) => next.columnVirtualizer.isScrolling
+) as typeof TableBodyCell
+
+const rootElement = document.getElementById('root')
+
+if (!rootElement) throw new Error('Failed to find the root element')
+
+ReactDOM.createRoot(rootElement).render(
+
+
+
+)
diff --git a/examples/react/virtualized-columns-expiremental/src/makeData.ts b/examples/react/virtualized-columns-expiremental/src/makeData.ts
new file mode 100644
index 0000000000..3fde072d12
--- /dev/null
+++ b/examples/react/virtualized-columns-expiremental/src/makeData.ts
@@ -0,0 +1,19 @@
+import { faker } from '@faker-js/faker'
+
+export const makeColumns = num =>
+ [...Array(num)].map((_, i) => {
+ return {
+ accessorKey: i.toString(),
+ header: 'Column ' + i.toString(),
+ size: Math.floor(Math.random() * 150) + 100,
+ }
+ })
+
+export const makeData = (num, columns) =>
+ [...Array(num)].map(() => ({
+ ...Object.fromEntries(
+ columns.map(col => [col.accessorKey, faker.person.firstName()])
+ ),
+ }))
+
+export type Person = ReturnType[0]
diff --git a/examples/react/virtualized-columns-expiremental/tsconfig.json b/examples/react/virtualized-columns-expiremental/tsconfig.json
new file mode 100644
index 0000000000..6d545f543f
--- /dev/null
+++ b/examples/react/virtualized-columns-expiremental/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/examples/react/virtualized-columns-expiremental/vite.config.js b/examples/react/virtualized-columns-expiremental/vite.config.js
new file mode 100644
index 0000000000..2e1361723a
--- /dev/null
+++ b/examples/react/virtualized-columns-expiremental/vite.config.js
@@ -0,0 +1,17 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import rollupReplace from '@rollup/plugin-replace'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [
+ rollupReplace({
+ preventAssignment: true,
+ values: {
+ __DEV__: JSON.stringify(true),
+ 'process.env.NODE_ENV': JSON.stringify('development'),
+ },
+ }),
+ react(),
+ ],
+})
diff --git a/examples/react/virtualized-columns/index.html b/examples/react/virtualized-columns/index.html
index 3fc40c9367..fa04f89341 100644
--- a/examples/react/virtualized-columns/index.html
+++ b/examples/react/virtualized-columns/index.html
@@ -8,6 +8,7 @@
+