diff --git a/examples/react/lanes/.gitignore b/examples/react/lanes/.gitignore
new file mode 100644
index 00000000..d451ff16
--- /dev/null
+++ b/examples/react/lanes/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
diff --git a/examples/react/lanes/README.md b/examples/react/lanes/README.md
new file mode 100644
index 00000000..b168d3c4
--- /dev/null
+++ b/examples/react/lanes/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/lanes/index.html b/examples/react/lanes/index.html
new file mode 100644
index 00000000..b3016791
--- /dev/null
+++ b/examples/react/lanes/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ React Virtual Lanes Example
+
+
+
+
+
+
diff --git a/examples/react/lanes/package.json b/examples/react/lanes/package.json
new file mode 100644
index 00000000..64032174
--- /dev/null
+++ b/examples/react/lanes/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "tanstack-react-virtual-example-lanes",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "serve": "vite preview --port 3001",
+ "start": "vite"
+ },
+ "dependencies": {
+ "@tanstack/react-pacer": "^0.2.0",
+ "@tanstack/react-virtual": "^3.13.12",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.23",
+ "@types/react-dom": "^18.3.7",
+ "@vitejs/plugin-react": "^4.5.2",
+ "vite": "^5.4.19"
+ }
+}
diff --git a/examples/react/lanes/src/index.css b/examples/react/lanes/src/index.css
new file mode 100644
index 00000000..c46155fa
--- /dev/null
+++ b/examples/react/lanes/src/index.css
@@ -0,0 +1,28 @@
+html {
+ font-family: sans-serif;
+ font-size: 14px;
+}
+
+body {
+ padding: 1rem;
+}
+
+.List {
+ border: 1px solid #e6e4dc;
+ max-width: 100%;
+}
+
+.ListItemEven,
+.ListItemOdd {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.ListItemEven {
+ background-color: #e6e4dc;
+}
+
+button {
+ border: 1px solid gray;
+}
diff --git a/examples/react/lanes/src/main.tsx b/examples/react/lanes/src/main.tsx
new file mode 100644
index 00000000..3fa5d354
--- /dev/null
+++ b/examples/react/lanes/src/main.tsx
@@ -0,0 +1,354 @@
+import * as React from 'react'
+import * as ReactDOM from 'react-dom/client'
+
+import './index.css'
+
+import { useVirtualizer } from '@tanstack/react-virtual'
+import { debounce } from '@tanstack/react-pacer'
+
+function App() {
+ return (
+
+
+ Lanes are useful when you are trying to draw a grid of
+ items, where each row is split into multiple columns.
+
+
+
+
+
Lanes
+
+
+
+
Lanes Gaps
+
+
+
+
Resizable Container Lanes
+
+
+
+
+
+ {process.env.NODE_ENV === 'development' ? (
+
+ Notice: You are currently running React in
+ development mode. Rendering performance will be slightly degraded
+ until this application is built for production.
+
+ ) : null}
+
+ )
+}
+
+function LanesVirtualizer() {
+ const [numLanes, setNumLanes] = React.useState(4)
+ const parentRef = React.useRef(null)
+
+ const rowVirtualizer = useVirtualizer({
+ count: 10000,
+ getScrollElement: () => parentRef.current,
+ estimateSize: () => 35,
+ overscan: 5,
+ lanes: numLanes,
+ })
+
+ return (
+ <>
+
+ Num Lanes
+ {
+ setNumLanes(Number(e.target.value))
+ rowVirtualizer.measure()
+ }}
+ />
+
+
+
+
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => (
+
+ Cell {virtualRow.index}
+
+ ))}
+
+
+ >
+ )
+}
+
+function GapVirtualizer() {
+ const parentRef = React.useRef(null)
+ const [numLanes, setNumLanes] = React.useState(4)
+ const [rowGap, setRowGap] = React.useState(10)
+ const [columnGap, setColumnGap] = React.useState(10)
+
+ const rowVirtualizer = useVirtualizer({
+ count: 10000,
+ getScrollElement: () => parentRef.current,
+ estimateSize: () => 35,
+ overscan: 5,
+ lanes: numLanes,
+ gap: rowGap,
+ })
+
+ return (
+ <>
+
+ Num Lanes
+ {
+ setNumLanes(Number(e.target.value))
+ rowVirtualizer.measure()
+ }}
+ />
+ Row Gap
+ {
+ setRowGap(Number(e.target.value))
+ rowVirtualizer.measure()
+ }}
+ />
+ Column Gap
+ {
+ setColumnGap(Number(e.target.value))
+ rowVirtualizer.measure()
+ }}
+ />
+
+
+
+
+
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => {
+ return (
+
+ Cell {virtualRow.index}
+
+ )
+ })}
+
+
+ >
+ )
+}
+
+const CELL_WIDTH = 100
+function ResizeVirtualizer() {
+ const parentRef = React.useRef(null)
+ const [numLanes, setNumLanes] = React.useState(4)
+ const [rowGap, setRowGap] = React.useState(10)
+ const [columnGap, setColumnGap] = React.useState(10)
+
+ const rowVirtualizer = useVirtualizer({
+ count: 10000,
+ getScrollElement: () => parentRef.current,
+ estimateSize: () => 35,
+ overscan: 5,
+ lanes: numLanes,
+ gap: rowGap,
+ })
+
+ React.useEffect(() => {
+ if (!parentRef.current) return
+ // debounce not necessary
+ const debouncedOnResize = debounce(
+ (entries: Array) => {
+ const rect = entries.at(0)?.contentRect
+ if (!rect) return
+ const { width } = rect
+ setNumLanes(Math.floor(width / CELL_WIDTH))
+ rowVirtualizer.measure()
+ },
+ {
+ wait: 50,
+ },
+ )
+ const resizeObserver = new ResizeObserver((entries) => {
+ debouncedOnResize(entries)
+ })
+ resizeObserver.observe(parentRef.current)
+ return () => {
+ resizeObserver.disconnect()
+ }
+ }, [rowVirtualizer])
+
+ return (
+ <>
+
+ Num Lanes
+
+ Row Gap
+ {
+ setRowGap(Number(e.target.value))
+ rowVirtualizer.measure()
+ }}
+ />
+ Column Gap
+ {
+ setColumnGap(Number(e.target.value))
+ rowVirtualizer.measure()
+ }}
+ />
+
+
+
+
+
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => {
+ return (
+
+ Cell {virtualRow.index}
+
+ )
+ })}
+
+
+ >
+ )
+}
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/examples/react/lanes/tsconfig.json b/examples/react/lanes/tsconfig.json
new file mode 100644
index 00000000..87318025
--- /dev/null
+++ b/examples/react/lanes/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "composite": true,
+ "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/lanes/vite.config.js b/examples/react/lanes/vite.config.js
new file mode 100644
index 00000000..5a33944a
--- /dev/null
+++ b/examples/react/lanes/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 90d30066..1f4d461f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -695,6 +695,34 @@ importers:
specifier: ^5.4.19
version: 5.4.19(@types/node@24.5.2)(less@4.4.0)(sass@1.89.2)(terser@5.43.1)
+ examples/react/lanes:
+ dependencies:
+ '@tanstack/react-pacer':
+ specifier: ^0.2.0
+ version: 0.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@tanstack/react-virtual':
+ specifier: ^3.13.12
+ version: link:../../../packages/react-virtual
+ react:
+ specifier: ^18.3.1
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.3.1
+ version: 18.3.1(react@18.3.1)
+ devDependencies:
+ '@types/react':
+ specifier: ^18.3.23
+ version: 18.3.23
+ '@types/react-dom':
+ specifier: ^18.3.7
+ version: 18.3.7(@types/react@18.3.23)
+ '@vitejs/plugin-react':
+ specifier: ^4.5.2
+ version: 4.7.0(vite@5.4.19(@types/node@24.5.2)(less@4.4.0)(sass@1.89.2)(terser@5.43.1))
+ vite:
+ specifier: ^5.4.19
+ version: 5.4.19(@types/node@24.5.2)(less@4.4.0)(sass@1.89.2)(terser@5.43.1)
+
examples/react/padding:
dependencies:
'@tanstack/react-virtual':
@@ -3806,6 +3834,10 @@ packages:
resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==}
engines: {node: '>=12'}
+ '@tanstack/pacer@0.2.0':
+ resolution: {integrity: sha512-fUJs3NpSwtAL/tfq8kuYdgvm9HbbJvHsOG6aHY2dFDfff0NBFNwjvyGreWZZRPs2zgoIbr4nOk+rRV7aQgmf+A==}
+ engines: {node: '>=18'}
+
'@tanstack/publish-config@0.2.1':
resolution: {integrity: sha512-URVXmXwlZXL75AFyvyOORef1tv2f16dEaFntwLYnBHoKLQMxyWYRzQrnXooxO1xf+GidJuDSZSC6Rc9UX1aK7g==}
engines: {node: '>=18'}
@@ -3819,6 +3851,13 @@ packages:
'@tanstack/query-devtools@5.80.0':
resolution: {integrity: sha512-D6gH4asyjaoXrCOt5vG5Og/YSj0D/TxwNQgtLJIgWbhbWCC/emu2E92EFoVHh4ppVWg1qT2gKHvKyQBEFZhCuA==}
+ '@tanstack/react-pacer@0.2.0':
+ resolution: {integrity: sha512-KU5GtjkKSeNdYCilen5Dc+Pu/6BPQbsQshKrUUjrg7URyJIiGBCz6ZZFre1QjDz/aeUeqUJWMWSm+2Dsh64v+w==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ react: '>=16.8'
+ react-dom: '>=16.8'
+
'@tanstack/react-query@5.84.0':
resolution: {integrity: sha512-iPycFGLq5lltDE16Jf13Nx7SOvtfoopfOH/+Ahbdd+z4QqOfYu/SOkY86AVYVcKjneuqPxTm8e85lSGhwe0cog==}
peerDependencies:
@@ -11014,6 +11053,8 @@ snapshots:
dependencies:
remove-accents: 0.5.0
+ '@tanstack/pacer@0.2.0': {}
+
'@tanstack/publish-config@0.2.1':
dependencies:
'@commitlint/parse': 19.8.1
@@ -11029,6 +11070,12 @@ snapshots:
'@tanstack/query-devtools@5.80.0': {}
+ '@tanstack/react-pacer@0.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@tanstack/pacer': 0.2.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
'@tanstack/react-query@5.84.0(react@18.3.1)':
dependencies:
'@tanstack/query-core': 5.83.1