Repository: TanStack/virtual Branch: main Commit: c9397853af6c Files: 515 Total size: 550.6 KB Directory structure: gitextract_lr2rhb18/ ├── .changeset/ │ └── config.json ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ └── config.yml │ ├── pull_request_template.md │ ├── renovate.json │ └── workflows/ │ ├── autofix.yml │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs/ │ ├── api/ │ │ ├── virtual-item.md │ │ └── virtualizer.md │ ├── config.json │ ├── framework/ │ │ ├── angular/ │ │ │ └── angular-virtual.md │ │ ├── lit/ │ │ │ └── lit-virtual.md │ │ ├── react/ │ │ │ └── react-virtual.md │ │ ├── solid/ │ │ │ └── solid-virtual.md │ │ ├── svelte/ │ │ │ └── svelte-virtual.md │ │ └── vue/ │ │ └── vue-virtual.md │ ├── installation.md │ └── introduction.md ├── eslint.config.js ├── examples/ │ ├── angular/ │ │ ├── dynamic/ │ │ │ ├── .devcontainer/ │ │ │ │ └── devcontainer.json │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── app.config.ts │ │ │ │ │ ├── app.routes.ts │ │ │ │ │ ├── column-virtualizer-dynamic.component.ts │ │ │ │ │ ├── grid-virtualizer-dynamic.component.ts │ │ │ │ │ ├── row-virtualizer-dynamic-window.component.ts │ │ │ │ │ ├── row-virtualizer-dynamic.component.ts │ │ │ │ │ └── utils.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ └── styles.css │ │ │ ├── tsconfig.app.json │ │ │ └── tsconfig.json │ │ ├── fixed/ │ │ │ ├── .devcontainer/ │ │ │ │ └── devcontainer.json │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── column-virtualizer-fixed.component.ts │ │ │ │ │ ├── grid-virtualizer-fixed.component.ts │ │ │ │ │ └── row-virtualizer-fixed.component.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ └── styles.css │ │ │ ├── tsconfig.app.json │ │ │ └── tsconfig.json │ │ ├── infinite-scroll/ │ │ │ ├── .devcontainer/ │ │ │ │ └── devcontainer.json │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ └── app.component.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ └── styles.css │ │ │ ├── tsconfig.app.json │ │ │ └── tsconfig.json │ │ ├── padding/ │ │ │ ├── .devcontainer/ │ │ │ │ └── devcontainer.json │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── column-virtualizer-padding.component.ts │ │ │ │ │ ├── grid-virtualizer-padding.component.ts │ │ │ │ │ └── row-virtualizer-padding.component.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ └── styles.css │ │ │ ├── tsconfig.app.json │ │ │ └── tsconfig.json │ │ ├── smooth-scroll/ │ │ │ ├── .devcontainer/ │ │ │ │ └── devcontainer.json │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ └── app.component.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ └── styles.css │ │ │ ├── tsconfig.app.json │ │ │ └── tsconfig.json │ │ ├── sticky/ │ │ │ ├── .devcontainer/ │ │ │ │ └── devcontainer.json │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ └── app.component.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ └── styles.css │ │ │ ├── tsconfig.app.json │ │ │ └── tsconfig.json │ │ ├── table/ │ │ │ ├── .devcontainer/ │ │ │ │ └── devcontainer.json │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ └── make-data.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ └── styles.css │ │ │ ├── tsconfig.app.json │ │ │ └── tsconfig.json │ │ ├── variable/ │ │ │ ├── .devcontainer/ │ │ │ │ └── devcontainer.json │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── angular.json │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── app/ │ │ │ │ │ ├── app.component.ts │ │ │ │ │ ├── column-virtualizer-variable.component.ts │ │ │ │ │ ├── grid-virtualizer-variable.component.ts │ │ │ │ │ └── row-virtualizer-variable.component.ts │ │ │ │ ├── index.html │ │ │ │ ├── main.ts │ │ │ │ └── styles.css │ │ │ ├── tsconfig.app.json │ │ │ └── tsconfig.json │ │ └── window/ │ │ ├── .devcontainer/ │ │ │ └── devcontainer.json │ │ ├── .gitignore │ │ ├── README.md │ │ ├── angular.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── app/ │ │ │ │ └── app.component.ts │ │ │ ├── index.html │ │ │ ├── main.ts │ │ │ └── styles.css │ │ ├── tsconfig.app.json │ │ └── tsconfig.json │ ├── lit/ │ │ ├── dynamic/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── index.css │ │ │ │ └── main.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.js │ │ └── fixed/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.css │ │ │ └── main.ts │ │ ├── tsconfig.json │ │ └── vite.config.js │ ├── react/ │ │ ├── dynamic/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── index.css │ │ │ │ └── main.tsx │ │ │ ├── tsconfig.json │ │ │ └── vite.config.js │ │ ├── fixed/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── index.css │ │ │ │ └── main.tsx │ │ │ ├── tsconfig.json │ │ │ └── vite.config.js │ │ ├── infinite-scroll/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── index.css │ │ │ │ └── main.tsx │ │ │ ├── tsconfig.json │ │ │ └── vite.config.js │ │ ├── padding/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── index.css │ │ │ │ └── main.tsx │ │ │ ├── tsconfig.json │ │ │ └── vite.config.js │ │ ├── scroll-padding/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── index.css │ │ │ │ └── main.tsx │ │ │ ├── tsconfig.json │ │ │ └── vite.config.js │ │ ├── smooth-scroll/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── index.css │ │ │ │ └── main.tsx │ │ │ ├── tsconfig.json │ │ │ └── vite.config.js │ │ ├── sticky/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── index.css │ │ │ │ └── main.tsx │ │ │ ├── tsconfig.json │ │ │ └── vite.config.js │ │ ├── table/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── index.css │ │ │ │ ├── main.tsx │ │ │ │ └── makeData.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.js │ │ ├── variable/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── index.css │ │ │ │ └── main.tsx │ │ │ ├── tsconfig.json │ │ │ └── vite.config.js │ │ └── window/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.css │ │ │ └── main.tsx │ │ ├── tsconfig.json │ │ └── vite.config.js │ ├── svelte/ │ │ ├── dynamic/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── App.svelte │ │ │ │ ├── ColumnVirtualizerDynamic.svelte │ │ │ │ ├── GridVirtualizerDynamic.svelte │ │ │ │ ├── RowVirtualizerDynamic.svelte │ │ │ │ ├── RowVirtualizerDynamicWindow.svelte │ │ │ │ ├── app.css │ │ │ │ ├── main.ts │ │ │ │ └── vite-env.d.ts │ │ │ ├── svelte.config.js │ │ │ ├── tsconfig.json │ │ │ ├── tsconfig.node.json │ │ │ └── vite.config.ts │ │ ├── fixed/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── App.svelte │ │ │ │ ├── ColumnVirtualizerFixed.svelte │ │ │ │ ├── GridVirtualizerFixed.svelte │ │ │ │ ├── RowVirtualizerFixed.svelte │ │ │ │ ├── app.css │ │ │ │ ├── main.ts │ │ │ │ └── vite-env.d.ts │ │ │ ├── svelte.config.js │ │ │ ├── tsconfig.json │ │ │ ├── tsconfig.node.json │ │ │ └── vite.config.ts │ │ ├── infinite-scroll/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── App.svelte │ │ │ │ ├── InfiniteRows.svelte │ │ │ │ ├── app.css │ │ │ │ ├── main.ts │ │ │ │ └── vite-env.d.ts │ │ │ ├── svelte.config.js │ │ │ ├── tsconfig.json │ │ │ ├── tsconfig.node.json │ │ │ └── vite.config.ts │ │ ├── smooth-scroll/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── App.svelte │ │ │ │ ├── app.css │ │ │ │ ├── main.ts │ │ │ │ └── vite-env.d.ts │ │ │ ├── svelte.config.js │ │ │ ├── tsconfig.json │ │ │ ├── tsconfig.node.json │ │ │ └── vite.config.ts │ │ ├── sticky/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── App.svelte │ │ │ │ ├── app.css │ │ │ │ ├── main.ts │ │ │ │ └── vite-env.d.ts │ │ │ ├── svelte.config.js │ │ │ ├── tsconfig.json │ │ │ ├── tsconfig.node.json │ │ │ └── vite.config.ts │ │ └── table/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.svelte │ │ │ ├── app.css │ │ │ ├── main.ts │ │ │ ├── makeData.ts │ │ │ └── vite-env.d.ts │ │ ├── svelte.config.js │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ └── vue/ │ ├── dynamic/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.vue │ │ │ ├── components/ │ │ │ │ ├── ColumnVirtualizerDynamic.vue │ │ │ │ ├── GridVirtualizerDynamic.vue │ │ │ │ ├── RowVirtualizerDynamic.vue │ │ │ │ ├── RowVirtualizerDynamicWindow.vue │ │ │ │ └── utils.ts │ │ │ ├── main.ts │ │ │ ├── style.css │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── fixed/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.vue │ │ │ ├── components/ │ │ │ │ ├── ColumnVirtualizerFixed.vue │ │ │ │ ├── GridVirtualizerFixed.vue │ │ │ │ └── RowVirtualizerFixed.vue │ │ │ ├── main.ts │ │ │ ├── style.css │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── infinite-scroll/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.vue │ │ │ ├── main.ts │ │ │ ├── style.css │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── padding/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.vue │ │ │ ├── components/ │ │ │ │ ├── ColumnVirtualizerPadding.vue │ │ │ │ ├── GridVirtualizerPadding.vue │ │ │ │ └── RowVirtualizerPadding.vue │ │ │ ├── main.ts │ │ │ ├── style.css │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── scroll-padding/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.vue │ │ │ ├── main.ts │ │ │ ├── style.css │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── smooth-scroll/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.vue │ │ │ ├── main.ts │ │ │ ├── style.css │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── sticky/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.vue │ │ │ ├── main.ts │ │ │ ├── style.css │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── table/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.vue │ │ │ ├── main.ts │ │ │ ├── makeData.ts │ │ │ ├── style.css │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ └── variable/ │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── App.vue │ │ ├── components/ │ │ │ ├── ColumnVirtualizerVariable.vue │ │ │ ├── GridVirtualizerVariable.vue │ │ │ ├── MasonryHorizontalVirtualizerVariable.vue │ │ │ ├── MasonryVerticalVirtualizerVariable.vue │ │ │ └── RowVirtualizerVariable.vue │ │ ├── main.ts │ │ ├── style.css │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── knip.json ├── media/ │ └── logo.sketch ├── nx.json ├── package.json ├── packages/ │ ├── angular-virtual/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── eslint.config.js │ │ ├── ng-package.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── proxy.ts │ │ │ └── types.ts │ │ ├── tsconfig.build.json │ │ └── tsconfig.json │ ├── lit-virtual/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── tests/ │ │ │ └── index.test.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── react-virtual/ │ │ ├── CHANGELOG.md │ │ ├── e2e/ │ │ │ └── app/ │ │ │ ├── measure-element/ │ │ │ │ ├── index.html │ │ │ │ └── main.tsx │ │ │ ├── scroll/ │ │ │ │ ├── index.html │ │ │ │ └── main.tsx │ │ │ ├── smooth-scroll/ │ │ │ │ ├── index.html │ │ │ │ └── main.tsx │ │ │ ├── stale-index/ │ │ │ │ ├── index.html │ │ │ │ └── main.tsx │ │ │ ├── test/ │ │ │ │ ├── measure-element.spec.ts │ │ │ │ ├── scroll.spec.ts │ │ │ │ ├── smooth-scroll.spec.ts │ │ │ │ └── stale-index.spec.ts │ │ │ ├── tsconfig.json │ │ │ └── vite.config.ts │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── playwright.config.ts │ │ ├── src/ │ │ │ └── index.tsx │ │ ├── tests/ │ │ │ ├── index.test.tsx │ │ │ └── test-setup.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── solid-virtual/ │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.tsx │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── svelte-virtual/ │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── svelte.config.js │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── virtual-core/ │ │ ├── CHANGELOG.md │ │ ├── eslint.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── tests/ │ │ │ └── index.test.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── vue-virtual/ │ ├── CHANGELOG.md │ ├── eslint.config.js │ ├── package.json │ ├── src/ │ │ └── index.ts │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-workspace.yaml ├── prettier.config.js ├── scripts/ │ └── verify-links.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json", "changelog": [ "@svitejs/changesets-changelog-github-compact", { "repo": "TanStack/virtual" } ], "commit": false, "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", "fixed": [], "linked": [], "ignore": [], "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { "onlyUpdatePeerDependentsWhenOutOfRange": true } } ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto eol=lf ================================================ FILE: .github/FUNDING.yml ================================================ github: tannerlinsley ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: 🐛 Bug Report description: Report a reproducible bug or regression body: - type: markdown attributes: value: | Thank you for reporting an issue :pray:. This issue tracker is for reporting reproducible bugs or regression's found in [tanstack-virtual](https://github.com/tanstack/virtual) If you have a question about how to achieve or implement something and are struggling, please post a question inside of tanstack-virtual's [Discussions tab](https://github.com/tanstack/virtual/discussions) instead of filing an issue. Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already: - tanstack-virtual's [Discussions tab](https://github.com/tanstack/virtual/discussions) - tanstack-virtual's [Open Issues](https://github.com/tanstack/virtual/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) - tanstack-virtual's [Closed Issues](https://github.com/tanstack/virtual/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed) The more information you fill in, the better the community can help you. - type: textarea id: description attributes: label: Describe the bug description: Provide a clear and concise description of the challenge you are running into. validations: required: true - type: input id: link attributes: label: Your minimal, reproducible example description: | Please add a link to a minimal reproduction. Note: - Your bug may get fixed much faster if we can run your code and it doesn't have dependencies other than React. - To create a shareable code example for web, you can use CodeSandbox (https://codesandbox.io/s/new) or Stackblitz (https://stackblitz.com/). - Please make sure the example is complete and runnable without prior dependencies and free of unnecessary abstractions - Feel free to fork any of the official CodeSandbox examples to reproduce your issue: https://github.com/tanstack/virtual/tree/main/examples/ - For React Native, you can use: https://snack.expo.dev/ - For TypeScript related issues only, a TypeScript Playground link might be sufficient: https://www.typescriptlang.org/play - Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve. placeholder: | e.g. Code Sandbox, Stackblitz, Expo Snack or TypeScript playground validations: required: true - type: textarea id: steps attributes: label: Steps to reproduce description: Describe the steps we have to take to reproduce the behavior. placeholder: | 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error validations: required: true - type: textarea id: expected attributes: label: Expected behavior description: Provide a clear and concise description of what you expected to happen. placeholder: | As a user, I expected **_ behavior but i am seeing _** validations: required: true - type: dropdown attributes: options: - Every time - Often - Sometimes - Only once label: How often does this bug happen? description: | Following the repro steps above, how easily are you able to reproduce this bug? options: - Every time - Often - Sometimes - Only once - type: textarea id: screenshots_or_videos attributes: label: Screenshots or Videos description: | If applicable, add screenshots or a video to help explain your problem. For more information on the supported file image/file types and the file size limits, please refer to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files placeholder: | You can drag your video or image files inside of this editor ↓ - type: textarea id: platform attributes: label: Platform description: | Please let us know which Operting System, Browser and Browser version you were using when the issue occurred. placeholder: | - OS: [e.g. macOS, Windows, Linux, iOS, Android] - Browser: [e.g. Chrome, Safari, Firefox, React Native] - Version: [e.g. 91.1] validations: required: true - type: input id: library-version attributes: label: tanstack-virtual version description: | Please let us know the exact version of tanstack-virtual you were using when the issue occurred. Please don't just put in "latest", as this is subject to change. placeholder: | e.g. v3.30.1 validations: required: true - type: input id: ts-version attributes: label: TypeScript version description: | If you are using TypeScript, please let us know the exact version of TypeScript you were using when the issue occurred. placeholder: | e.g. v5.2.2 - type: textarea id: additional attributes: label: Additional context description: Add any other context about the problem here. - type: checkboxes id: agrees-to-terms attributes: label: Terms & Code of Conduct description: By submitting this issue, you agree to follow our Code of Conduct and can verify that you have followed the requirements outlined above to the best of your ability. options: - label: I agree to follow this project's Code of Conduct required: true - label: I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed. required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: 🤔 Feature Requests & Questions url: https://github.com/TanStack/virtual/discussions about: Please ask and answer questions here. - name: 💬 Community Chat url: https://discord.gg/mQd7egN about: A dedicated discord server hosted by TanStack - name: 🦋 TanStack Bluesky url: https://bsky.app/profile/tanstack.com about: Stay up to date with new releases of our libraries ================================================ FILE: .github/pull_request_template.md ================================================ ## 🎯 Changes ## ✅ Checklist - [ ] I have followed the steps in the [Contributing guide](https://github.com/TanStack/virtual/blob/main/CONTRIBUTING.md). - [ ] I have tested this code locally with `pnpm run test:pr`. ## 🚀 Release Impact - [ ] This change affects published code, and I have generated a [changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). - [ ] This change is docs/CI/dev-only (no release). ================================================ FILE: .github/renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "configMigration": true, "extends": [ "config:recommended", "group:allNonMajor", "schedule:weekly", ":approveMajorUpdates", ":automergeMinor", ":disablePeerDependencies", ":maintainLockFilesMonthly", ":semanticCommits", ":semanticCommitTypeAll(chore)" ], "ignorePresets": [":ignoreModulesAndTests"], "labels": ["dependencies"], "rangeStrategy": "bump", "postUpdateOptions": ["pnpmDedupe"], "ignoreDeps": ["@types/node", "node", "typescript"] } ================================================ FILE: .github/workflows/autofix.yml ================================================ name: autofix.ci # needed to securely identify the workflow on: pull_request: push: branches: [main, alpha, beta] concurrency: group: ${{ github.workflow }}-${{ github.event.number || github.ref }} cancel-in-progress: true permissions: contents: read jobs: autofix: name: autofix runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6.0.2 - name: Setup Tools uses: tanstack/config/.github/setup@main - name: Fix formatting run: pnpm format - name: Apply fixes uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef with: commit-message: 'ci: apply automated fixes' ================================================ FILE: .github/workflows/pr.yml ================================================ name: PR on: pull_request: concurrency: group: ${{ github.workflow }}-${{ github.event.number || github.ref }} cancel-in-progress: true env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} permissions: contents: read pull-requests: write jobs: test: name: Test runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - name: Setup Tools uses: tanstack/config/.github/setup@main - name: Get base and head commits for `nx affected` uses: nrwl/nx-set-shas@v4.4.0 with: main-branch-name: main - name: Install Playwright browsers run: pnpm exec playwright install chromium - name: Run Checks run: pnpm run test:pr preview: name: Preview runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6.0.2 - name: Setup Tools uses: tanstack/config/.github/setup@main - name: Build Packages run: pnpm run build:all - name: Publish Previews run: pnpx pkg-pr-new publish --pnpm --compact './packages/*' --template './examples/*/*' provenance: name: Provenance runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6.0.2 - name: Check Provenance uses: danielroe/provenance-action@v0.1.1 with: fail-on-downgrade: true version-preview: name: Version Preview runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6.0.2 - name: Setup Tools uses: TanStack/config/.github/setup@main - name: Changeset Preview uses: TanStack/config/.github/changeset-preview@main ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: branches: [main, alpha, beta] concurrency: group: ${{ github.workflow }}-${{ github.event.number || github.ref }} cancel-in-progress: true env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} permissions: contents: write id-token: write pull-requests: write jobs: release: name: Release if: github.repository_owner == 'TanStack' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - name: Setup Tools uses: tanstack/config/.github/setup@main - name: Install Playwright browsers run: pnpm exec playwright install chromium - name: Run Tests run: pnpm run test:ci - name: Run Changesets (version or publish) id: changesets uses: changesets/action@v1.7.0 with: version: pnpm run changeset:version publish: pnpm run changeset:publish commit: 'ci: Version Packages' title: 'ci: Version Packages' - name: Comment on PRs about release if: steps.changesets.outputs.published == 'true' uses: TanStack/config/.github/comment-on-release@main with: published-packages: ${{ steps.changesets.outputs.publishedPackages }} ================================================ FILE: .gitignore ================================================ # See https://help.github.com/ignore-files/ for more about ignoring files. # dependencies node_modules package-lock.json yarn.lock # builds build coverage dist # misc .DS_Store .env .env.local .env.development.local .env.test.local .env.production.local .next npm-debug.log* yarn-debug.log* yarn-error.log* .history size-plugin.json stats-hydration.json stats.json stats.html .vscode/settings.json *.log .cache .idea .nx/cache .nx/workspace-data .pnpm-store .tsup .svelte-kit vite.config.js.timestamp-* vite.config.ts.timestamp-* # Playwright test artifacts test-results/ playwright-report/ *.log ================================================ FILE: .npmrc ================================================ provenance=true ================================================ FILE: .nvmrc ================================================ 24.8.0 ================================================ FILE: .prettierignore ================================================ **/.next **/.nx/cache **/.svelte-kit **/build **/coverage **/dist **/docs **/codemods/**/__testfixtures__ pnpm-lock.yaml ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing ## Questions If you have questions about implementation details, help or support, then please use our dedicated community forum at [GitHub Discussions](https://github.com/TanStack/virtual/discussions) **PLEASE NOTE:** If you choose to instead open an issue for your question, your issue will be immediately closed and redirected to the forum. ## Reporting Issues If you have found what you think is a bug, please [file an issue](https://github.com/TanStack/virtual/issues/new/choose). **PLEASE NOTE:** Issues that are identified as implementation questions or non-issues will be immediately closed and redirected to [GitHub Discussions](https://github.com/TanStack/virtual/discussions) ## Suggesting new features If you are here to suggest a feature, first create an issue if it does not already exist. From there, we will discuss use-cases for the feature and then finally discuss how it could be implemented. ## Development If you have been assigned to fix an issue or develop a new feature, please follow these steps to get started: - Fork this repository. - Install dependencies ```bash pnpm install ``` - We use [pnpm](https://pnpm.io/) v9 for package management (run in case of pnpm-related issues). ```bash corepack enable && corepack prepare ``` - We use [nvm](https://github.com/nvm-sh/nvm) to manage node versions - please make sure to use the version mentioned in `.nvmrc` ```bash nvm use ``` - Build all packages. ```bash pnpm build:all ``` - Run development server. ```bash pnpm run watch ``` - Implement your changes and tests to files in the `src/` directory and corresponding test files. - Document your changes in the appropriate doc page. - Git stage your required changes and commit (see below commit guidelines). - Submit PR for review. ### Editing the docs locally and previewing the changes The documentations for all the TanStack projects are hosted on [tanstack.com](https://tanstack.com), which is a TanStack Start application (https://github.com/TanStack/tanstack.com). You need to run this app locally to preview your changes in the `TanStack/virtual` docs. > [!NOTE] > The website fetches the doc pages from GitHub in production, and searches for them at `../virtual/docs` in development. Your local clone of `TanStack/virtual` needs to be in the same directory as the local clone of `TanStack/tanstack.com`. You can follow these steps to set up the docs for local development: 1. Make a new directory called `tanstack`. ```sh mkdir tanstack ``` 2. Enter that directory and clone the [`TanStack/virtual`](https://github.com/TanStack/virtual) and [`TanStack/tanstack.com`](https://github.com/TanStack/tanstack.com) repos. ```sh cd tanstack git clone git@github.com:TanStack/virtual.git # We probably don't need all the branches and commit history # from the `tanstack.com` repo, so let's just create a shallow # clone of the latest version of the `main` branch. # Read more about shallow clones here: # https://github.blog/2020-12-21-get-up-to-speed-with-partial-clone-and-shallow-clone/#user-content-shallow-clones git clone git@github.com:TanStack/tanstack.com.git --depth=1 --single-branch --branch=main ``` > [!NOTE] > Your `tanstack` directory should look like this: > > ``` > tanstack/ > | > +-- virtual/ (<-- this directory cannot be called anything else!) > | > +-- tanstack.com/ > ``` 3. Enter the `tanstack/tanstack.com` directory, install the dependencies and run the app in dev mode: ```sh cd tanstack.com pnpm i # The app will run on https://localhost:3000 by default pnpm dev ``` 4. Now you can visit http://localhost:3000/virtual/latest/docs/overview in the browser and see the changes you make in `TanStack/virtual/docs` there. > [!WARNING] > You will need to update the `docs/config.json` file (in `TanStack/virtual`) if you add a new documentation page! You can see the whole process in the screen capture below: https://github.com/fulopkovacs/form/assets/43729152/9d35a3c3-8153-4e74-9cb2-af275f7a269b ### Running examples - Make sure you've installed the dependencies in the repo's root directory. ```bash pnpm install ``` - If you want to run the example against your local changes, run below in the repo's root directory. Otherwise, it will be run against the latest TanStack Virtual release. ```bash pnpm run watch ``` - Run below in the selected examples' directory. ```bash pnpm run dev ``` #### Note on standalone execution If you want to run an example without installing dependencies for the whole repo, just follow instructions from the example's README.md file. It will be then run against the latest TanStack Virtual release. ## Online one-click setup You can use Gitpod (An Online Open Source VS Code like IDE which is free for Open Source) for developing online. With a single click it will start a workspace and automatically: - clone the `TanStack/virtual` repo. - install all the dependencies in `/` and `/docs`. - run below in the root(`/`) to Auto-build files. ```bash npm start ``` - run below in `/docs`. ```bash npm run dev ``` [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/TanStack/virtual) ## Changesets This repo uses [Changesets](https://github.com/changesets/changesets) to automate releases. If your PR should release a new package version (patch, minor, or major), please run run `pnpm changeset` and commit the file. If needed, changeset descriptions can be more descriptive, and will be included in the changelog. If your PR affects docs, examples, styles, etc., you probably don't need to generate a changeset. ## Pull requests Maintainers merge pull requests by squashing all commits and editing the commit message if necessary using the GitHub user interface. Use an appropriate commit type. Be especially careful with breaking changes. ## Releases For each new commit added to `main`, a GitHub Workflow is triggered which runs the [Changesets Action](https://github.com/changesets/action). This generates a preview PR showing the impact of all changesets. When this PR is merged, the package will be published to NPM. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021-present Tanner Linsley Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================
Tanstack Virtual

npm downloads github stars bundle size
semantic-release Best of JS Follow @TanStack
### [Become a Sponsor!](https://github.com/sponsors/tannerlinsley/)
# TanStack Virtual A headless, framework‑agnostic virtualization library for rendering massive lists, grids, and tables at 60FPS while giving you full control over markup and styles. - Framework‑agnostic & headless - Virtualizes vertical, horizontal & grid layouts with a single hook/function - Lightweight (10–15kb) yet powerful, with dynamic & measured sizing support - Smooth 60FPS scrolling with sticky items and window‑scrolling utilities ### Read the docs → ## Get Involved - We welcome issues and pull requests! - Participate in [GitHub discussions](https://github.com/TanStack/virtual/discussions) - Chat with the community on [Discord](https://discord.com/invite/WrRKjPJ) - See [CONTRIBUTING.md](./CONTRIBUTING.md) for setup instructions ## Partners
CodeRabbit Cloudflare
Virtual & you?

We're looking for TanStack Virtual Partners to join our mission! Partner with us to push the boundaries of TanStack Virtual and build amazing things together.

LET'S CHAT
## Explore the TanStack Ecosystem - TanStack Config – Tooling for JS/TS packages - TanStack DB – Reactive sync client store - TanStack DevTools – Unified devtools panel - TanStack Form – Type‑safe form state - TanStack Pacer – Debouncing, throttling, batching
- TanStack Query – Async state & caching - TanStack Ranger – Range & slider primitives - TanStack Router – Type‑safe routing, caching & URL state - TanStack Start – Full‑stack SSR & streaming - TanStack Store – Reactive data store - TanStack Table – Headless datagrids … and more at TanStack.com » ================================================ FILE: docs/api/virtual-item.md ================================================ --- title: VirtualItem --- The `VirtualItem` object represents a single item returned by the virtualizer. It contains information you need to render the item in the coordinate space within your virtualizer's scrollElement and other helpful properties/functions. ```tsx export interface VirtualItem { key: string | number | bigint index: number start: number end: number size: number } ``` The following properties and methods are available on each VirtualItem object: ### `key` ```tsx key: string | number | bigint ``` The unique key for the item. By default this is the item index, but should be configured via the `getItemKey` Virtualizer option. ### `index` ```tsx index: number ``` The index of the item. ### `start` ```tsx start: number ``` The starting pixel offset for the item. This is usually mapped to a css property or transform like `top/left` or `translateX/translateY`. ### `end` ```tsx end: number ``` The ending pixel offset for the item. This value is not necessary for most layouts, but can be helpful so we've provided it anyway. ### `size` ```tsx size: number ``` The size of the item. This is usually mapped to a css property like `width/height`. Before an item is measured with the `VirtualItem.measureElement` method, this will be the estimated size returned from your `estimateSize` virtualizer option. After an item is measured (if you choose to measure it at all), this value will be the number returned by your `measureElement` virtualizer option (which by default is configured to measure elements with `getBoundingClientRect()`). ### `lane` ```tsx lane: number ``` The lane index of the item. In regular lists it will always be set to `0` but becomes useful for masonry layouts (see variable examples for more details). ================================================ FILE: docs/api/virtualizer.md ================================================ --- title: Virtualizer --- The `Virtualizer` class is the core of TanStack Virtual. Virtualizer instances are usually created for you by your framework adapter, but you do receive the virtualizer directly. ```tsx export class Virtualizer { constructor(options: VirtualizerOptions) } ``` ## Required Options ### `count` ```tsx count: number ``` The total number of items to virtualize. ### `getScrollElement` ```tsx getScrollElement: () => TScrollElement ``` A function that returns the scrollable element for the virtualizer. It may return null if the element is not available yet. ### `estimateSize` ```tsx estimateSize: (index: number) => number ``` > 🧠 If you are dynamically measuring your elements, it's recommended to estimate the largest possible size (width/height, within comfort) of your items. This will help the virtualizer calculate more accurate initial positions. This function is passed the index of each item and should return the actual size (or estimated size if you will be dynamically measuring items with `virtualItem.measureElement`) for each item. This measurement should return either the width or height depending on the orientation of your virtualizer. ## Optional Options ### `enabled` ```tsx enabled?: boolean ``` Set to `false` to disable scrollElement observers and reset the virtualizer's state ### `debug` ```tsx debug?: boolean ``` Set to `true` to enable debug logs ### `initialRect` ```tsx initialRect?: Rect ``` The initial `Rect` of the scrollElement. This is mostly useful if you need to run the virtualizer in an SSR environment, otherwise the initialRect will be calculated on mount by the `observeElementRect` implementation. ### `onChange` ```tsx onChange?: (instance: Virtualizer, sync: boolean) => void ``` A callback function that fires when the virtualizer's internal state changes. It's passed the virtualizer instance and the sync parameter. The sync parameter indicates whether scrolling is currently in progress. It is `true` when scrolling is ongoing, and `false` when scrolling has stopped or other actions (such as resizing) are being performed. ### `overscan` ```tsx overscan?: number ``` The number of items to render above and below the visible area. Increasing this number will increase the amount of time it takes to render the virtualizer, but might decrease the likelihood of seeing slow-rendering blank items at the top and bottom of the virtualizer when scrolling. The default value is `1`. ### `horizontal` ```tsx horizontal?: boolean ``` Set this to `true` if your virtualizer is oriented horizontally. ### `paddingStart` ```tsx paddingStart?: number ``` The padding to apply to the start of the virtualizer in pixels. ### `paddingEnd` ```tsx paddingEnd?: number ``` The padding to apply to the end of the virtualizer in pixels. ### `scrollPaddingStart` ```tsx scrollPaddingStart?: number ``` The padding to apply to the start of the virtualizer in pixels when scrolling to an element. ### `scrollPaddingEnd` ```tsx scrollPaddingEnd?: number ``` The padding to apply to the end of the virtualizer in pixels when scrolling to an element. ### `initialOffset` ```tsx initialOffset?: number | (() => number) ``` The position where the list is scrolled to on render. This is useful if you are rendering the virtualizer in a SSR environment or are conditionally rendering the virtualizer. ### `getItemKey` ```tsx getItemKey?: (index: number) => Key ``` This function is passed the index of each item and should return a unique key for that item. The default functionality of this function is to return the index of the item, but you should override this when possible to return a unique identifier for each item across the entire set. **Note:** The virtualizer automatically invalidates its measurement cache when measurement-affecting options change, ensuring `getTotalSize()` and other measurements return fresh values. While the virtualizer intelligently tracks which options actually affect measurements, it's still better to memoize `getItemKey` (e.g., using `useCallback` in React) to avoid unnecessary recalculations. ### `rangeExtractor` ```tsx rangeExtractor?: (range: Range) => number[] ``` This function receives visible range indexes and should return array of indexes to render. This is useful if you need to add or remove items from the virtualizer manually regardless of the visible range, eg. rendering sticky items, headers, footers, etc. The default range extractor implementation will return the visible range indexes and is exported as `defaultRangeExtractor`. ### `scrollToFn` ```tsx scrollToFn?: ( offset: number, options: { adjustments?: number; behavior?: 'auto' | 'smooth' }, instance: Virtualizer, ) => void ``` An optional function that (if provided) should implement the scrolling behavior for your scrollElement. It will be called with the following arguments: - An `offset` (in pixels) to scroll towards. - An object indicating whether there was a difference between the estimated size and actual size (`adjustments`) and/or whether scrolling was called with a smooth animation (`behaviour`). - The virtualizer instance itself. Note that built-in scroll implementations are exported as `elementScroll` and `windowScroll`, which are automatically configured by the framework adapter functions like `useVirtualizer` or `useWindowVirtualizer`. ### `observeElementRect` ```tsx observeElementRect: ( instance: Virtualizer, cb: (rect: Rect) => void, ) => void | (() => void) ``` An optional function that if provided is called when the scrollElement changes and should implement the initial measurement and continuous monitoring of the scrollElement's `Rect` (an object with `width` and `height`). It's called with the instance (which also gives you access to the scrollElement via `instance.scrollElement`. Built-in implementations are exported as `observeElementRect` and `observeWindowRect` which are automatically configured for you by your framework adapter's exported functions like `useVirtualizer` or `useWindowVirtualizer`. ### `observeElementOffset` ```tsx observeElementOffset: ( instance: Virtualizer, cb: (offset: number) => void, ) => void | (() => void) ``` An optional function that if provided is called when the scrollElement changes and should implement the initial measurement and continuous monitoring of the scrollElement's scroll offset (a number). It's called with the instance (which also gives you access to the scrollElement via `instance.scrollElement`. Built-in implementations are exported as `observeElementOffset` and `observeWindowOffset` which are automatically configured for you by your framework adapter's exported functions like `useVirtualizer` or `useWindowVirtualizer`. ### `measureElement` ```tsx measureElement?: ( element: TItemElement, entry: ResizeObserverEntry | undefined, instance: Virtualizer, ) => number ``` This optional function is called when the virtualizer needs to dynamically measure the size (width or height) of an item. > 🧠 You can use `instance.options.horizontal` to determine if the width or height of the item should be measured. ### `scrollMargin` ```tsx scrollMargin?: number ``` With this option, you can specify where the scroll offset should originate. Typically, this value represents the space between the beginning of the scrolling element and the start of the list. This is especially useful in common scenarios such as when you have a header preceding a window virtualizer or when multiple virtualizers are utilized within a single scrolling element. If you are using absolute positioning of elements, you should take into account the `scrollMargin` in your CSS transform: ```tsx transform: `translateY(${ virtualRow.start - rowVirtualizer.options.scrollMargin }px)` ``` To dynamically measure value for `scrollMargin` you can use `getBoundingClientRect()` or ResizeObserver. This is helpful in scenarios when items above your virtual list might change their height. ### `gap` ```tsx gap?: number ``` This option allows you to set the spacing between items in the virtualized list. It's particularly useful for maintaining a consistent visual separation between items without having to manually adjust each item's margin or padding. The value is specified in pixels. ### `lanes` ```tsx lanes: number ``` The number of lanes the list is divided into (aka columns for vertical lists and rows for horizontal lists). ### `isScrollingResetDelay` ```tsx isScrollingResetDelay: number ``` This option allows you to specify the duration to wait after the last scroll event before resetting the isScrolling instance property. The default value is 150 milliseconds. The implementation of this option is driven by the need for a reliable mechanism to handle scrolling behavior across different browsers. Until all browsers uniformly support the scrollEnd event. ### `useScrollendEvent` ```tsx useScrollendEvent: boolean ``` Determines whether to use the native scrollend event to detect when scrolling has stopped. If set to false, a debounced fallback is used to reset the isScrolling instance property after isScrollingResetDelay milliseconds. The default value is `false`. The implementation of this option is driven by the need for a reliable mechanism to handle scrolling behavior across different browsers. Until all browsers uniformly support the scrollEnd event. ### `isRtl` ```tsx isRtl: boolean ``` Whether to invert horizontal scrolling to support right-to-left language locales. ### `useAnimationFrameWithResizeObserver` ```tsx useAnimationFrameWithResizeObserver: boolean ``` **Default:** `false` When enabled, defers ResizeObserver measurement processing to the next animation frame using `requestAnimationFrame`. **Important:** This option typically **should not be enabled** in most cases. ResizeObserver callbacks already execute at an optimal time in the browser's rendering pipeline (after layout, before paint), and the measurements provided in the callback are pre-computed by the browser without causing additional reflows. **Potential use cases:** - If you're performing heavy DOM mutations in response to size changes and want to batch them with the next render cycle - As a workaround for the "ResizeObserver loop completed with undelivered notifications" error (though this usually indicates a deeper issue that should be fixed) **Tradeoffs:** - **Adds ~16ms delay:** Measurements are deferred to the next frame, which can cause visual artifacts, stale measurements, or slower time-to-interactive - **No batching benefit:** ResizeObserver already batches multiple element resizes into a single callback - **Defeats optimization:** The browser has already computed the measurements synchronously; deferring them provides no performance benefit for reading values Only enable this option if you have a specific reason and have measured that it improves your use case. ## Virtualizer Instance The following properties and methods are available on the virtualizer instance: ### `options` ```tsx options: readonly Required> ``` The current options for the virtualizer. This property is updated via your framework adapter and is read-only. ### `scrollElement` ```tsx scrollElement: readonly TScrollElement | null ``` The current scrollElement for the virtualizer. This property is updated via your framework adapter and is read-only. ### `getVirtualItems` ```tsx type getVirtualItems = () => VirtualItem[] ``` Returns the virtual items for the current state of the virtualizer. ### `getVirtualIndexes` ```tsx type getVirtualIndexes = () => number[] ``` Returns the virtual row indexes for the current state of the virtualizer. ### `scrollToOffset` ```tsx scrollToOffset: ( toOffset: number, options?: { align?: 'start' | 'center' | 'end' | 'auto', behavior?: 'auto' | 'smooth' } ) => void ``` Scrolls the virtualizer to the pixel offset provided. You can optionally pass an alignment mode to anchor the scroll to a specific part of the scrollElement. ### `scrollToIndex` ```tsx scrollToIndex: ( index: number, options?: { align?: 'start' | 'center' | 'end' | 'auto', behavior?: 'auto' | 'smooth' } ) => void ``` Scrolls the virtualizer to the items of the index provided. You can optionally pass an alignment mode to anchor the scroll to a specific part of the scrollElement. > 🧠 During smooth scrolling, the virtualizer only measures items within a buffer range around the scroll target. Items far from the target are skipped to prevent their size changes from shifting the target position and breaking the smooth animation. > > Because of this, the preferred layout strategy for smooth scrolling is **block translation** — translate the entire rendered block using the first item's `start` offset, rather than positioning each item independently with absolute positioning. This ensures items stay correctly positioned relative to each other even when some measurements are skipped. ### `scrollBy` ```tsx scrollBy: ( delta: number, options?: { behavior?: 'auto' | 'smooth' } ) => void ``` Scrolls the virtualizer by the specified number of pixels relative to the current scroll position. ### `getTotalSize` ```tsx getTotalSize: () => number ``` Returns the total size in pixels for the virtualized items. This measurement will incrementally change if you choose to dynamically measure your elements as they are rendered. ### `measure` ```tsx measure: () => void ``` Resets any prev item measurements. ### `measureElement` ```tsx measureElement: (el: TItemElement | null) => void ``` Measures the element using your configured `measureElement` virtualizer option. You are responsible for calling this in your virtualizer markup when the component is rendered (eg. using something like React's ref callback prop) also adding `data-index` ```tsx
...
``` By default the `measureElement` virtualizer option is configured to measure elements with `getBoundingClientRect()`. ### `resizeItem` ```tsx resizeItem: (index: number, size: number) => void ``` Change the virtualized item's size manually. Use this function to manually set the size calculated for this index. Useful in occations when using some custom morphing transition and you know the morphed item's size beforehand. You can also use this method with a throttled ResizeObserver instead of `Virtualizer.measureElement` to reduce re-rendering. > ⚠️ Please be aware that manually changing the size of an item when using `Virtualizer.measureElement` to monitor that item, will result in unpredictable behaviour as the `Virtualizer.measureElement` is also changing the size. However you can use one of resizeItem or measureElement in the same virtualizer instance but on different item indexes. ### `scrollRect` ```tsx scrollRect: Rect ``` Current `Rect` of the scroll element. ### `shouldAdjustScrollPositionOnItemSizeChange` ```tsx shouldAdjustScrollPositionOnItemSizeChange: undefined | ((item: VirtualItem, delta: number, instance: Virtualizer) => boolean) ``` The shouldAdjustScrollPositionOnItemSizeChange method enables fine-grained control over the adjustment of scroll position when the size of dynamically rendered items differs from the estimated size. When jumping in the middle of the list and scrolling backward new elements may have a different size than the initially estimated size. This discrepancy can cause subsequent items to shift, potentially disrupting the user's scrolling experience, particularly when navigating backward through the list. ### `isScrolling` ```tsx isScrolling: boolean ``` Boolean flag indicating if list is currently being scrolled. ### `scrollDirection` ```tsx scrollDirection: 'forward' | 'backward' | null ``` This option indicates the direction of scrolling, with possible values being 'forward' for scrolling downwards and 'backward' for scrolling upwards. The value is set to null when there is no active scrolling. ### `scrollOffset` ```tsx scrollOffset: number ``` This option represents the current scroll position along the scrolling axis. It is measured in pixels from the starting point of the scrollable area. ================================================ FILE: docs/config.json ================================================ { "$schema": "https://raw.githubusercontent.com/TanStack/tanstack.com/main/tanstack-docs-config.schema.json", "docSearch": { "appId": "", "indexName": "", "apiKey": "" }, "sections": [ { "label": "Getting Started", "children": [ { "label": "Introduction", "to": "introduction" }, { "label": "Installation", "to": "installation" } ], "frameworks": [ { "label": "react", "children": [ { "label": "React Virtual", "to": "framework/react/react-virtual" } ] }, { "label": "angular", "children": [ { "label": "Angular Virtual", "to": "framework/angular/angular-virtual" } ] }, { "label": "solid", "children": [ { "label": "Solid Virtual", "to": "framework/solid/solid-virtual" } ] }, { "label": "svelte", "children": [ { "label": "Svelte Virtual", "to": "framework/svelte/svelte-virtual" } ] }, { "label": "vue", "children": [ { "label": "Vue Virtual", "to": "framework/vue/vue-virtual" } ] } ] }, { "label": "Core APIs", "children": [ { "label": "Virtualizer", "to": "api/virtualizer" }, { "label": "VirtualItem", "to": "api/virtual-item" } ] }, { "label": "Examples", "children": [], "frameworks": [ { "label": "angular", "children": [ { "to": "framework/angular/examples/fixed", "label": "Fixed" }, { "to": "framework/angular/examples/variable", "label": "Variable" }, { "to": "framework/angular/examples/dynamic", "label": "Dynamic" }, { "to": "framework/angular/examples/padding", "label": "Padding" }, { "to": "framework/angular/examples/sticky", "label": "Sticky" }, { "to": "framework/angular/examples/infinite-scroll", "label": "Infinite Scroll" }, { "to": "framework/angular/examples/smooth-scroll", "label": "Smooth Scroll" }, { "to": "framework/angular/examples/table", "label": "Table" }, { "to": "framework/angular/examples/window", "label": "Window" } ] }, { "label": "react", "children": [ { "to": "framework/react/examples/fixed", "label": "Fixed" }, { "to": "framework/react/examples/variable", "label": "Variable" }, { "to": "framework/react/examples/dynamic", "label": "Dynamic" }, { "to": "framework/react/examples/padding", "label": "Padding" }, { "to": "framework/react/examples/sticky", "label": "Sticky" }, { "to": "framework/react/examples/infinite-scroll", "label": "Infinite Scroll" }, { "to": "framework/react/examples/smooth-scroll", "label": "Smooth Scroll" }, { "to": "framework/react/examples/table", "label": "Table" }, { "to": "framework/react/examples/window", "label": "Window" } ] }, { "label": "svelte", "children": [ { "to": "framework/svelte/examples/fixed", "label": "Fixed" }, { "to": "framework/svelte/examples/variable", "label": "Variable" }, { "to": "framework/svelte/examples/dynamic", "label": "Dynamic" }, { "to": "framework/svelte/examples/sticky", "label": "Sticky" }, { "to": "framework/svelte/examples/infinite-scroll", "label": "Infinite Scroll" }, { "to": "framework/svelte/examples/smooth-scroll", "label": "Smooth Scroll" }, { "to": "framework/svelte/examples/table", "label": "Table" } ] }, { "label": "vue", "children": [ { "to": "framework/vue/examples/fixed", "label": "Fixed" }, { "to": "framework/vue/examples/variable", "label": "Variable" }, { "to": "framework/vue/examples/dynamic", "label": "Dynamic" }, { "to": "framework/vue/examples/sticky", "label": "Sticky" }, { "to": "framework/vue/examples/infinite-scroll", "label": "Infinite Scroll" }, { "to": "framework/vue/examples/smooth-scroll", "label": "Smooth Scroll" }, { "to": "framework/vue/examples/table", "label": "Table" }, { "to": "framework/vue/examples/padding", "label": "Padding" }, { "to": "framework/vue/examples/scroll-padding", "label": "Scroll Padding" } ] }, { "label": "lit", "children": [ { "to": "framework/lit/examples/fixed", "label": "Fixed" }, { "to": "framework/lit/examples/dynamic", "label": "Dynamic" } ] } ] } ] } ================================================ FILE: docs/framework/angular/angular-virtual.md ================================================ --- title: Angular Virtual --- The `@tanstack/angular-virtual` adapter is a wrapper around the core virtual logic. ## `injectVirtualizer` ```ts function injectVirtualizer( options: PartialKeys< Omit, 'getScrollElement'>, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' > & { scrollElement: ElementRef | TScrollElement | undefined }, ): AngularVirtualizer ``` This function returns an `AngularVirtualizer` instance configured to work with an HTML element as the scrollElement. ## `injectWindowVirtualizer` ```ts function injectWindowVirtualizer( options: PartialKeys< VirtualizerOptions, | 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): AngularVirtualizer ``` This function returns a window-based `AngularVirtualizer` instance configured to work with the window as the scrollElement. ================================================ FILE: docs/framework/lit/lit-virtual.md ================================================ --- title: Lit Virtual --- The `@tanstack/lit-virtual` adapter is a wrapper around the core virtual logic. ## `createVirtualizer` ```tsx private virtualizerController = new VirtualizerController( options: PartialKeys< VirtualizerOptions, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' ) ``` This class stands for a standard `Virtualizer` instance configured to work with an HTML element as the scrollElement. This will create a Lit Controller which can be accessed in the element render method. ```tsx render() { const virtualizer = this.virtualizerController.getVirtualizer(); const virtualItems = virtualizer.getVirtualItems(); } ) ``` ## `createWindowVirtualizer` ```tsx private windowVirtualizerController = new WindowVirtualizerController( options: PartialKeys< VirtualizerOptions, 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' ``` This class stands of window-based `Virtualizer` instance configured to work with an HTML element as the scrollElement. ================================================ FILE: docs/framework/react/react-virtual.md ================================================ --- title: React Virtual --- The `@tanstack/react-virtual` adapter is a wrapper around the core virtual logic. ## `useVirtualizer` ```tsx function useVirtualizer( options: PartialKeys< ReactVirtualizerOptions, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): Virtualizer ``` This function returns a standard `Virtualizer` instance configured to work with an HTML element as the scrollElement. ## `useWindowVirtualizer` ```tsx function useWindowVirtualizer( options: PartialKeys< ReactVirtualizerOptions, | 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): Virtualizer ``` This function returns a window-based `Virtualizer` instance configured to work with the window as the scrollElement. ## React-Specific Options ### `useFlushSync` ```tsx type ReactVirtualizerOptions = VirtualizerOptions & { useFlushSync?: boolean } ``` Both `useVirtualizer` and `useWindowVirtualizer` accept a `useFlushSync` option that controls whether React's `flushSync` is used for synchronous updates. - **Type**: `boolean` - **Default**: `true` - **Description**: When `true`, the virtualizer will use `flushSync` from `react-dom` to ensure synchronous rendering during scroll events. This provides the most accurate scrolling behavior but may impact performance in some scenarios. #### When to disable `useFlushSync` You may want to set `useFlushSync: false` in the following scenarios: - **React 19 compatibility**: In React 19, you may see the following console warning when scrolling: ``` flushSync was called from inside a lifecycle method. React cannot flush when React is already rendering. Consider moving this call to a scheduler task or micro task. ``` Setting `useFlushSync: false` will eliminate this warning by allowing React to batch updates naturally. - **Performance optimization**: If you experience performance issues with rapid scrolling on lower-end devices - **Testing environments**: When running tests that don't require synchronous DOM updates - **Non-critical lists**: When slight visual delays during scrolling are acceptable for better overall performance #### Example ```tsx const virtualizer = useVirtualizer({ count: 10000, getScrollElement: () => parentRef.current, estimateSize: () => 50, useFlushSync: false, // Disable synchronous updates }) ``` ================================================ FILE: docs/framework/solid/solid-virtual.md ================================================ --- title: Solid Virtual --- The `@tanstack/solid-virtual` adapter is a wrapper around the core virtual logic. ## `createVirtualizer` ```tsx function createVirtualizer( options: PartialKeys< VirtualizerOptions, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): Virtualizer ``` This function returns a standard `Virtualizer` instance configured to work with an HTML element as the scrollElement. ## `createWindowVirtualizer` ```tsx function createWindowVirtualizer( options: PartialKeys< VirtualizerOptions, | 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): Virtualizer ``` This function returns a window-based `Virtualizer` instance configured to work with the window as the scrollElement. ================================================ FILE: docs/framework/svelte/svelte-virtual.md ================================================ --- title: Svelte Virtual --- The `@tanstack/svelte-virtual` adapter is a wrapper around the core virtual logic. ## `createVirtualizer` ```tsx function createVirtualizer( options: PartialKeys< VirtualizerOptions, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): Virtualizer ``` This function returns a standard `Virtualizer` instance configured to work with an HTML element as the scrollElement. ## `createWindowVirtualizer` ```tsx function createWindowVirtualizer( options: PartialKeys< VirtualizerOptions, | 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): Virtualizer ``` This function returns a window-based `Virtualizer` instance configured to work with the window as the scrollElement. ================================================ FILE: docs/framework/vue/vue-virtual.md ================================================ --- title: Vue Virtual --- The `@tanstack/vue-virtual` adapter is a wrapper around the core virtual logic. ## `useVirtualizer` ```tsx function useVirtualizer( options: PartialKeys< VirtualizerOptions, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): Virtualizer ``` This function returns a standard `Virtualizer` instance configured to work with an HTML element as the scrollElement. ## `useWindowVirtualizer` ```tsx function useWindowVirtualizer( options: PartialKeys< VirtualizerOptions, | 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): Virtualizer ``` This function returns a window-based `Virtualizer` instance configured to work with the window as the scrollElement. ================================================ FILE: docs/installation.md ================================================ --- title: Installation --- Before we dig in to the API, let's get you set up! Install your TanStack Virtual adapter as a dependency using your favorite npm package manager ## React Virtual ```bash npm install @tanstack/react-virtual ``` ## Solid Virtual ```bash npm install @tanstack/solid-virtual ``` ## Svelte Virtual ```bash npm install @tanstack/svelte-virtual ``` ## Vue Virtual ```bash npm install @tanstack/vue-virtual ``` ## Lit Virtual ```bash npm install @tanstack/lit-virtual ``` ## Angular Virtual ```bash npm install @tanstack/angular-virtual ``` ## Virtual Core (no framework) ```bash npm install @tanstack/virtual-core ``` ================================================ FILE: docs/introduction.md ================================================ --- title: Introduction --- TanStack Virtual is a headless UI utility for virtualizing long lists of elements in JS/TS, React, Vue, Svelte, Solid, Lit, and Angular. It is not a component therefore does not ship with or render any markup or styles for you. While this requires a bit of markup and styles from you, you will retain 100% control over your styles, design and implementation. ## The Virtualizer At the heart of TanStack Virtual is the `Virtualizer`. Virtualizers can be oriented on either the vertical (default) or horizontal axes which makes it possible to achieve vertical, horizontal and even grid-like virtualization by combining the two axis configurations together. Here is just a quick example of what it looks like to virtualize a long list within a div using TanStack Virtual in React: ```tsx import { useVirtualizer } from '@tanstack/react-virtual'; function App() { // The scrollable element for your list const parentRef = React.useRef(null) // The virtualizer const rowVirtualizer = useVirtualizer({ count: 10000, getScrollElement: () => parentRef.current, estimateSize: () => 35, }) return ( <> {/* The scrollable element for your list */}
{/* The large inner element to hold all of the items */}
{/* Only the visible items in the virtualizer, manually positioned to be in view */} {rowVirtualizer.getVirtualItems().map((virtualItem) => (
Row {virtualItem.index}
))}
) } ``` Let's dig into some more examples! ================================================ FILE: eslint.config.js ================================================ // @ts-check import { tanstackConfig } from '@tanstack/eslint-config' export default [ ...tanstackConfig, { name: 'tanstack/temp', rules: { '@typescript-eslint/naming-convention': 'off', '@typescript-eslint/no-unnecessary-condition': 'off', '@typescript-eslint/no-unsafe-function-type': 'off', 'no-self-assign': 'off', }, }, ] ================================================ FILE: examples/angular/dynamic/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:18" } ================================================ FILE: examples/angular/dynamic/.gitignore ================================================ # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db ================================================ FILE: examples/angular/dynamic/README.md ================================================ # @tanstack/virtualExampleAngularDynamic This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: examples/angular/dynamic/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "newProjectRoot": "projects", "projects": { "@tanstack/virtual-example-angular-dynamic": { "projectType": "application", "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/tanstack/virtual-example-angular-dynamic", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico"], "styles": ["src/styles.css"], "scripts": [] }, "configurations": { "production": { "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "buildTarget": "@tanstack/virtual-example-angular-dynamic:build:production" }, "development": { "buildTarget": "@tanstack/virtual-example-angular-dynamic:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "buildTarget": "@tanstack/virtual-example-angular-dynamic:build" } } } } } } ================================================ FILE: examples/angular/dynamic/package.json ================================================ { "name": "@tanstack/virtual-example-angular-dynamic", "private": true, "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "dependencies": { "@angular/animations": "^18.1.0", "@angular/common": "^18.1.0", "@angular/compiler": "^18.1.0", "@angular/core": "^18.1.0", "@angular/forms": "^18.1.0", "@angular/platform-browser": "^18.1.0", "@angular/platform-browser-dynamic": "^18.1.0", "@angular/router": "^18.1.0", "@faker-js/faker": "^8.4.1", "@tanstack/angular-virtual": "^4.0.11", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.1" }, "devDependencies": { "@angular-devkit/build-angular": "^18.1.0", "@angular/cli": "^18.1.0", "@angular/compiler-cli": "^18.1.0", "typescript": "5.4.5" } } ================================================ FILE: examples/angular/dynamic/src/app/app.component.ts ================================================ import { Component } from '@angular/core' import { RouterLink, RouterOutlet } from '@angular/router' @Component({ selector: 'app-root', standalone: true, imports: [RouterLink, RouterOutlet], template: `

These components are using dynamic sizes. This means that each element's exact dimensions are unknown when rendered. An estimated dimension is used to get an a initial measurement, then this measurement is readjusted on the fly as each element is rendered.

`, styles: [], }) export class AppComponent {} ================================================ FILE: examples/angular/dynamic/src/app/app.config.ts ================================================ import { ApplicationConfig } from '@angular/core' import { provideRouter } from '@angular/router' import { routes } from './app.routes' export const appConfig: ApplicationConfig = { providers: [provideRouter(routes)], } ================================================ FILE: examples/angular/dynamic/src/app/app.routes.ts ================================================ import { Routes } from '@angular/router' import { RowVirtualizerDynamic } from './row-virtualizer-dynamic.component' import { GridVirtualizerDynamic } from './grid-virtualizer-dynamic.component' import { ColumnVirtualizerDynamic } from './column-virtualizer-dynamic.component' import { RowVirtualizerDynamicWindow } from './row-virtualizer-dynamic-window.component' export const routes: Routes = [ { path: '', component: RowVirtualizerDynamic, }, { path: 'window-list', component: RowVirtualizerDynamicWindow, }, { path: 'columns', component: ColumnVirtualizerDynamic, }, { path: 'grid', component: GridVirtualizerDynamic, }, ] ================================================ FILE: examples/angular/dynamic/src/app/column-virtualizer-dynamic.component.ts ================================================ import { ChangeDetectionStrategy, Component, ElementRef, effect, viewChild, viewChildren, } from '@angular/core' import { injectVirtualizer } from '@tanstack/angular-virtual' import { sentences } from './utils' @Component({ standalone: true, selector: 'column-virtualizer-dynamic', changeDetection: ChangeDetectionStrategy.OnPush, template: `

Columns

@for (col of virtualizer.getVirtualItems(); track col.index) {
Column {{ col.index }}
{{ sentences[col.index] }}
}
`, styles: ` .scroll-container { height: 400px; width: 400px; overflow: auto; } `, }) export class ColumnVirtualizerDynamic { scrollElement = viewChild>('scrollElement') virtualItems = viewChildren>('virtualItem') sentences = sentences count = this.sentences.length #measureItems = effect( () => this.virtualItems().forEach((el) => { this.virtualizer.measureElement(el.nativeElement) }), { allowSignalWrites: true }, ) virtualizer = injectVirtualizer(() => ({ horizontal: true, scrollElement: this.scrollElement(), count: this.count, estimateSize: () => 100, overscan: 5, })) } ================================================ FILE: examples/angular/dynamic/src/app/grid-virtualizer-dynamic.component.ts ================================================ import { ChangeDetectionStrategy, Component, ElementRef, afterNextRender, computed, effect, signal, viewChild, viewChildren, } from '@angular/core' import { injectVirtualizer, injectWindowVirtualizer, } from '@tanstack/angular-virtual' import { generateColumns, generateData } from './utils' @Component({ standalone: true, selector: 'grid-virtualizer-dynamic', changeDetection: ChangeDetectionStrategy.OnPush, template: `

Grid

@for (row of rowVirtualizer.getVirtualItems(); track row.key) {
@for (col of columnVirtualizer.getVirtualItems(); track col.key) {
{{ row.index === 0 ? columns[col.index].name : data[row.index][col.index] }}
}
}
`, styles: ` .scroll-container { overflow: auto; } `, }) export class GridVirtualizerDynamic { scrollElement = viewChild>('scrollElement') columns = generateColumns(30) data = generateData(this.columns) parentOffset = signal(0) constructor() { afterNextRender(() => this.parentOffset.set(this.scrollElement()!.nativeElement.offsetTop), ) } getColumnWidth = (index: number) => this.columns[index].width rowVirtualizer = injectWindowVirtualizer(() => ({ count: this.data.length, estimateSize: () => 350, overscan: 5, scrollMargin: this.parentOffset(), })) columnVirtualizer = injectVirtualizer(() => ({ horizontal: true, scrollElement: this.scrollElement(), count: this.columns.length, estimateSize: this.getColumnWidth, overscan: 5, })) width = computed( () => { const virtualColumns = this.columnVirtualizer.getVirtualItems() return virtualColumns.length > 0 ? [ virtualColumns[0].start, this.columnVirtualizer.getTotalSize() - virtualColumns[virtualColumns.length - 1].end, ] : [0, 0] }, { equal: (a, b) => a[0] === b[0] && a[1] === b[1] }, ) virtualRows = viewChildren>('virtualRow') #measureItems = effect( () => this.virtualRows().forEach((el) => { this.rowVirtualizer.measureElement(el.nativeElement) }), { allowSignalWrites: true }, ) } ================================================ FILE: examples/angular/dynamic/src/app/row-virtualizer-dynamic-window.component.ts ================================================ import { ChangeDetectionStrategy, Component, ElementRef, afterNextRender, effect, signal, untracked, viewChild, viewChildren, } from '@angular/core' import { injectWindowVirtualizer } from '@tanstack/angular-virtual' import { sentences } from './utils' @Component({ standalone: true, selector: 'row-virtualizer-dynamic', changeDetection: ChangeDetectionStrategy.OnPush, template: `
@for (row of virtualizer.getVirtualItems(); track row.index) {
Row {{ row.index }}
{{ sentences[row.index] }}
}
`, styles: ` .scroll-container { height: 400px; width: 400px; overflow-y: auto; contain: 'strict'; } `, }) export class RowVirtualizerDynamicWindow { scrollElement = viewChild>('scrollElement') parentOffset = signal(0) constructor() { afterNextRender(() => this.parentOffset.set(this.scrollElement()!.nativeElement.offsetTop), ) } virtualItems = viewChildren>('virtualItem') sentences = sentences count = this.sentences.length #measureItems = effect( () => this.virtualItems().forEach((el) => { this.virtualizer.measureElement(el.nativeElement) }), { allowSignalWrites: true }, ) virtualizer = injectWindowVirtualizer(() => ({ count: this.count, estimateSize: () => 150, scrollMargin: this.parentOffset(), })) } ================================================ FILE: examples/angular/dynamic/src/app/row-virtualizer-dynamic.component.ts ================================================ import { ChangeDetectionStrategy, Component, ElementRef, effect, viewChild, viewChildren, } from '@angular/core' import { AngularVirtualizer, injectVirtualizer, } from '@tanstack/angular-virtual' import { sentences } from './utils' @Component({ standalone: true, selector: 'row-virtualizer-dynamic', changeDetection: ChangeDetectionStrategy.OnPush, template: `

Rows


@for (row of virtualizer.getVirtualItems(); track row.index) {
Row {{ row.index }}
{{ sentences[row.index] }}
}
`, styles: ` .scroll-container { height: 400px; width: 400px; overflow-y: auto; contain: 'strict'; } `, }) export class RowVirtualizerDynamic { scrollElement = viewChild>('scrollElement') virtualItems = viewChildren>('virtualItem') sentences = sentences count = this.sentences.length #measureItems = effect( () => this.virtualItems().forEach((el) => { this.virtualizer.measureElement(el.nativeElement) }), { allowSignalWrites: true }, ) virtualizer = injectVirtualizer(() => ({ scrollElement: this.scrollElement(), count: this.count, estimateSize: () => 120, })) } ================================================ FILE: examples/angular/dynamic/src/app/utils.ts ================================================ import { faker } from '@faker-js/faker' export const generateRandomNumber = (min: number, max: number) => faker.number.int({ min, max }) // 1000 because 10000 takes many seconds export const sentences = new Array(1000) .fill(true) .map(() => faker.lorem.sentence(generateRandomNumber(20, 70))) interface Column { key: string name: string width: number } export const generateColumns = (count: number) => { return new Array(count).fill(0).map((_, i) => { const key: string = i.toString() return { key, name: `Column ${i}`, width: generateRandomNumber(75, 300), } }) } export const generateData = (columns: Column[], count = 300) => { return new Array(count).fill(0).map((_, rowIndex) => columns.reduce((acc, _curr, colIndex) => { // simulate dynamic size cells const val = faker.lorem.lines(((rowIndex + colIndex) % 10) + 1) acc.push(val) return acc }, []), ) } ================================================ FILE: examples/angular/dynamic/src/index.html ================================================ @tanstack/virtualExampleAngularDynamic ================================================ FILE: examples/angular/dynamic/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { appConfig } from './app/app.config' import { AppComponent } from './app/app.component' bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)) ================================================ FILE: examples/angular/dynamic/src/styles.css ================================================ *, *:before, *:after { box-sizing: border-box; } html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .list { border: 1px solid #e6e4dc; max-width: 100%; } .list-item-even { background-color: #e6e4dc; } .list-item-odd { background-color: #fff; } button { border: 1px solid gray; } ================================================ FILE: examples/angular/dynamic/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/dynamic/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"], "paths": { "@angular/*": ["./node_modules/@angular/*"] } }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true } } ================================================ FILE: examples/angular/fixed/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:18" } ================================================ FILE: examples/angular/fixed/.gitignore ================================================ # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db ================================================ FILE: examples/angular/fixed/README.md ================================================ # @tanstack/virtualExampleAngularFixed This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: examples/angular/fixed/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "projects": { "@tanstack/virtual-example-angular-fixed": { "projectType": "application", "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/tanstack/virtual-example-angular-fixed", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico"], "styles": ["src/styles.css"], "scripts": [] }, "configurations": { "production": { "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "buildTarget": "@tanstack/virtual-example-angular-fixed:build:production" }, "development": { "buildTarget": "@tanstack/virtual-example-angular-fixed:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "buildTarget": "@tanstack/virtual-example-angular-fixed:build" } } } } } } ================================================ FILE: examples/angular/fixed/package.json ================================================ { "name": "@tanstack/virtual-example-angular-fixed", "private": true, "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "dependencies": { "@angular/animations": "^18.1.0", "@angular/common": "^18.1.0", "@angular/compiler": "^18.1.0", "@angular/core": "^18.1.0", "@angular/forms": "^18.1.0", "@angular/platform-browser": "^18.1.0", "@angular/platform-browser-dynamic": "^18.1.0", "@angular/router": "^18.1.0", "@tanstack/angular-virtual": "^4.0.11", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.1" }, "devDependencies": { "@angular-devkit/build-angular": "^18.1.0", "@angular/cli": "^18.1.0", "@angular/compiler-cli": "^18.1.0", "typescript": "5.4.5" } } ================================================ FILE: examples/angular/fixed/src/app/app.component.ts ================================================ import { ChangeDetectionStrategy, Component } from '@angular/core' import { ColumnVirtualizerFixed } from './column-virtualizer-fixed.component' import { GridVirtualizerFixed } from './grid-virtualizer-fixed.component' import { RowVirtualizerFixed } from './row-virtualizer-fixed.component' @Component({ selector: 'app-root', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ColumnVirtualizerFixed, GridVirtualizerFixed, RowVirtualizerFixed], template: `

These components are using fixed sizes. This means that every element's dimensions are hard-coded to the same value and never change.

`, }) export class AppComponent {} ================================================ FILE: examples/angular/fixed/src/app/column-virtualizer-fixed.component.ts ================================================ import { ChangeDetectionStrategy, Component, ElementRef, viewChild, } from '@angular/core' import { injectVirtualizer } from '@tanstack/angular-virtual' @Component({ standalone: true, selector: 'column-virtualizer-fixed', changeDetection: ChangeDetectionStrategy.OnPush, template: `

Columns

@for (col of virtualizer.getVirtualItems(); track col.index) {
Col {{ col.index }}
}
`, styles: ` .scroll-container { height: 100px; width: 400px; overflow: auto; } `, }) export class ColumnVirtualizerFixed { scrollElement = viewChild>('scrollElement') virtualizer = injectVirtualizer(() => ({ horizontal: true, scrollElement: this.scrollElement(), count: 10000, estimateSize: () => 100, overscan: 5, })) } ================================================ FILE: examples/angular/fixed/src/app/grid-virtualizer-fixed.component.ts ================================================ import { ChangeDetectionStrategy, Component, ElementRef, viewChild, } from '@angular/core' import { injectVirtualizer } from '@tanstack/angular-virtual' @Component({ standalone: true, selector: 'grid-virtualizer-fixed', changeDetection: ChangeDetectionStrategy.OnPush, template: `

Grid

@for ( row of rowVirtualizer.getVirtualItems(); track row.index; let rowEven = $even ) { @for ( col of columnVirtualizer.getVirtualItems(); track col.index; let colEven = $even ) {
Cell {{ row.index }}, {{ col.index }}
} }
`, styles: ` .scroll-container { height: 500px; width: 500px; overflow: auto; } `, }) export class GridVirtualizerFixed { scrollElement = viewChild>('scrollElement') rowVirtualizer = injectVirtualizer(() => ({ scrollElement: this.scrollElement(), count: 10000, estimateSize: () => 35, overscan: 5, })) columnVirtualizer = injectVirtualizer(() => ({ horizontal: true, scrollElement: this.scrollElement(), count: 10000, estimateSize: () => 100, overscan: 5, })) } ================================================ FILE: examples/angular/fixed/src/app/row-virtualizer-fixed.component.ts ================================================ import { ChangeDetectionStrategy, Component, ElementRef, viewChild, } from '@angular/core' import { injectVirtualizer } from '@tanstack/angular-virtual' @Component({ standalone: true, selector: 'row-virtualizer-fixed', changeDetection: ChangeDetectionStrategy.OnPush, template: `

Rows

@for (row of virtualizer.getVirtualItems(); track row.index) {
Row {{ row.index }}
}
`, styles: ` .scroll-container { height: 200px; width: 400px; overflow: auto; } `, }) export class RowVirtualizerFixed { scrollElement = viewChild>('scrollElement') virtualizer = injectVirtualizer(() => ({ scrollElement: this.scrollElement(), count: 10000, estimateSize: () => 35, overscan: 5, })) } ================================================ FILE: examples/angular/fixed/src/index.html ================================================ @tanstack/virtualExampleAngularFixed ================================================ FILE: examples/angular/fixed/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { AppComponent } from './app/app.component' bootstrapApplication(AppComponent).catch((err) => console.error(err)) ================================================ FILE: examples/angular/fixed/src/styles.css ================================================ *, *:before, *:after { box-sizing: border-box; } html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .list { border: 1px solid #e6e4dc; max-width: 100%; } .list-item-even, .list-item-odd { display: flex; align-items: center; justify-content: center; } .list-item-even { background-color: #e6e4dc; } .list-item-odd { background-color: #fff; } ================================================ FILE: examples/angular/fixed/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/fixed/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true } } ================================================ FILE: examples/angular/infinite-scroll/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:18" } ================================================ FILE: examples/angular/infinite-scroll/.gitignore ================================================ # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db ================================================ FILE: examples/angular/infinite-scroll/README.md ================================================ # @tanstack/virtualExampleAngularInfiniteScroll This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: examples/angular/infinite-scroll/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "projects": { "@tanstack/virtual-example-angular-infinite-scroll": { "projectType": "application", "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/tanstack/virtual-example-angular-infinite-scroll", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico"], "styles": ["src/styles.css"], "scripts": [] }, "configurations": { "production": { "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "buildTarget": "@tanstack/virtual-example-angular-infinite-scroll:build:production" }, "development": { "buildTarget": "@tanstack/virtual-example-angular-infinite-scroll:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "buildTarget": "@tanstack/virtual-example-angular-infinite-scroll:build" } } } } } } ================================================ FILE: examples/angular/infinite-scroll/package.json ================================================ { "name": "@tanstack/virtual-example-angular-infinite-scroll", "private": true, "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "dependencies": { "@angular/animations": "^18.1.0", "@angular/common": "^18.1.0", "@angular/compiler": "^18.1.0", "@angular/core": "^18.1.0", "@angular/forms": "^18.1.0", "@angular/platform-browser": "^18.1.0", "@angular/platform-browser-dynamic": "^18.1.0", "@angular/router": "^18.1.0", "@tanstack/angular-query-experimental": "5.80.7", "@tanstack/angular-virtual": "^4.0.11", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.1" }, "devDependencies": { "@angular-devkit/build-angular": "^18.1.0", "@angular/cli": "^18.1.0", "@angular/compiler-cli": "^18.1.0", "typescript": "5.4.5" } } ================================================ FILE: examples/angular/infinite-scroll/src/app/app.component.ts ================================================ import { ChangeDetectionStrategy, Component, ElementRef, computed, effect, viewChild, } from '@angular/core' import { injectVirtualizer } from '@tanstack/angular-virtual' import { QueryClient, injectInfiniteQuery, provideQueryClient, } from '@tanstack/angular-query-experimental' async function fetchServerPage( limit: number, offset: number = 0, ): Promise<{ rows: string[]; nextOffset: number }> { const rows = new Array(limit) .fill(0) .map((e, i) => `Async loaded row #${i + offset * limit}`) await new Promise((r) => setTimeout(r, 500)) return { rows, nextOffset: offset + 1 } } @Component({ selector: 'infinite-scroll', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `

This infinite scroll example uses Angular Query's injectInfiniteScroll function to fetch infinite data from a posts endpoint and then a rowVirtualizer is used along with a loader-row placed at the bottom of the list to trigger the next page to load.

@if (query.isLoading()) {

Loading...

} @else if (query.isError()) { Error: {{ query.error()!.message }} } @else {
@for (row of virtualizer.getVirtualItems(); track row.index) {
{{ row.index > allRows().length - 1 ? query.hasNextPage() ? 'Loading more...' : 'Nothing more to load' : allRows()[row.index] }}
}
} @if (query.isFetching() && !query.isFetchingNextPage()) {
Background Updating...
} `, styles: ` .scroll-container { height: 500px; width: 100%; overflow: auto; } `, providers: [provideQueryClient(new QueryClient())], }) export class InfiniteScrollComponent { query = injectInfiniteQuery(() => ({ queryKey: ['rows'], queryFn: ({ pageParam }) => fetchServerPage(10, pageParam), initialPageParam: 0, getNextPageParam: (_lastGroup, groups) => groups.length, })) allRows = computed( () => this.query.data()?.pages.flatMap((d) => d.rows) ?? [], ) scrollElement = viewChild>('scrollElement') virtualizer = injectVirtualizer(() => ({ scrollElement: this.scrollElement(), count: this.query.hasNextPage() ? this.allRows().length + 1 : this.allRows().length, estimateSize: () => 100, overscan: 5, })) #fetchNextPage = effect( () => { const lastItem = this.virtualizer.getVirtualItems()[ this.virtualizer.getVirtualItems().length - 1 ] if (!lastItem) { return } if ( lastItem.index >= this.allRows().length - 1 && this.query.hasNextPage() && !this.query.isFetchingNextPage() ) { this.query.fetchNextPage() } }, { allowSignalWrites: true }, ) } @Component({ selector: 'app-root', standalone: true, imports: [InfiniteScrollComponent], template: '', }) export class AppComponent {} ================================================ FILE: examples/angular/infinite-scroll/src/index.html ================================================ @tanstack/virtualExampleAngularInfiniteScroll ================================================ FILE: examples/angular/infinite-scroll/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { AppComponent } from './app/app.component' bootstrapApplication(AppComponent).catch((err) => console.error(err)) ================================================ FILE: examples/angular/infinite-scroll/src/styles.css ================================================ *, *:before, *:after { box-sizing: border-box; } html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .list { border: 1px solid #e6e4dc; max-width: 100%; } .list-item-even, .list-item-odd { display: flex; align-items: center; justify-content: center; } .list-item-even { background-color: #e6e4dc; } .list-item-odd { background-color: #fff; } button { border: 1px solid gray; } ================================================ FILE: examples/angular/infinite-scroll/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/infinite-scroll/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true } } ================================================ FILE: examples/angular/padding/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:18" } ================================================ FILE: examples/angular/padding/.gitignore ================================================ # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db ================================================ FILE: examples/angular/padding/README.md ================================================ # @tanstack/virtualExampleAngularPadding This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: examples/angular/padding/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "projects": { "@tanstack/virtual-example-angular-padding": { "projectType": "application", "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/tanstack/virtual-example-angular-padding", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico"], "styles": ["src/styles.css"], "scripts": [] }, "configurations": { "production": { "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "buildTarget": "@tanstack/virtual-example-angular-padding:build:production" }, "development": { "buildTarget": "@tanstack/virtual-example-angular-padding:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "buildTarget": "@tanstack/virtual-example-angular-padding:build" } } } } } } ================================================ FILE: examples/angular/padding/package.json ================================================ { "name": "@tanstack/virtual-example-angular-padding", "private": true, "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "dependencies": { "@angular/animations": "^18.1.0", "@angular/common": "^18.1.0", "@angular/compiler": "^18.1.0", "@angular/core": "^18.1.0", "@angular/forms": "^18.1.0", "@angular/platform-browser": "^18.1.0", "@angular/platform-browser-dynamic": "^18.1.0", "@angular/router": "^18.1.0", "@tanstack/angular-virtual": "^4.0.11", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.1" }, "devDependencies": { "@angular-devkit/build-angular": "^18.1.0", "@angular/cli": "^18.1.0", "@angular/compiler-cli": "^18.1.0", "typescript": "5.4.5" } } ================================================ FILE: examples/angular/padding/src/app/app.component.ts ================================================ import { ChangeDetectionStrategy, Component } from '@angular/core' import { ColumnVirtualizerPadding } from './column-virtualizer-padding.component' import { GridVirtualizerPadding } from './grid-virtualizer-padding.component' import { RowVirtualizerPadding } from './row-virtualizer-padding.component' @Component({ selector: 'app-root', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ ColumnVirtualizerPadding, GridVirtualizerPadding, RowVirtualizerPadding, ], template: `

These components are using dynamic sizes. This means that each element's exact dimensions are unknown when rendered. An estimated dimension is used to get an a initial measurement, then this measurement is readjusted on the fly as each element is rendered. Each component has padding at the beginning and end of its scroll container.

`, styles: [], }) export class AppComponent { rows = new Array(10000) .fill(true) .map(() => 25 + Math.round(Math.random() * 100)) columns = new Array(10000) .fill(true) .map(() => 75 + Math.round(Math.random() * 100)) } ================================================ FILE: examples/angular/padding/src/app/column-virtualizer-padding.component.ts ================================================ import { ChangeDetectionStrategy, Component, ElementRef, effect, input, viewChild, viewChildren, } from '@angular/core' import { injectVirtualizer } from '@tanstack/angular-virtual' @Component({ standalone: true, selector: 'column-virtualizer-padding', changeDetection: ChangeDetectionStrategy.OnPush, template: `

Columns

@for (col of virtualizer.getVirtualItems(); track col.index) {
Column {{ col.index }}
}
`, styles: ` .scroll-container { height: 400px; width: 400px; overflow: auto; } `, }) export class ColumnVirtualizerPadding { columns = input.required() scrollElement = viewChild>('scrollElement') virtualItems = viewChildren>('virtualItem') #measureItems = effect( () => this.virtualItems().forEach((el) => { this.virtualizer.measureElement(el.nativeElement) }), { allowSignalWrites: true }, ) virtualizer = injectVirtualizer(() => ({ horizontal: true, scrollElement: this.scrollElement(), count: this.columns().length, estimateSize: () => 100, overscan: 5, paddingStart: 100, paddingEnd: 100, })) } ================================================ FILE: examples/angular/padding/src/app/grid-virtualizer-padding.component.ts ================================================ import { ChangeDetectionStrategy, Component, ElementRef, effect, input, signal, viewChild, viewChildren, } from '@angular/core' import { injectVirtualizer } from '@tanstack/angular-virtual' @Component({ standalone: true, selector: 'grid-virtualizer-padding', changeDetection: ChangeDetectionStrategy.OnPush, template: `

Grid

@if (show()) {
@for (row of rowVirtualizer.getVirtualItems(); track row.index) { @for (col of columnVirtualizer.getVirtualItems(); track col.index) {
Cell {{ row.index }}, {{ col.index }}
} }
} `, styles: ` .scroll-container { height: 400px; width: 500px; overflow: auto; } `, }) export class GridVirtualizerPadding { rows = input.required() columns = input.required() scrollElement = viewChild>('scrollElement') rowVirtualizer = injectVirtualizer(() => ({ scrollElement: this.scrollElement(), count: this.rows().length, estimateSize: (index) => this.rows()[index]!, overscan: 5, paddingStart: 200, paddingEnd: 200, indexAttribute: 'data-rowindex', })) columnVirtualizer = injectVirtualizer(() => ({ horizontal: true, scrollElement: this.scrollElement(), count: this.columns().length, estimateSize: (index) => this.columns()[index]!, overscan: 5, paddingStart: 200, paddingEnd: 200, indexAttribute: 'data-colindex', })) virtualItems = viewChildren>('virtualItem') #measureItems = effect( () => this.virtualItems().forEach((el) => { this.rowVirtualizer.measureElement(el.nativeElement) this.columnVirtualizer.measureElement(el.nativeElement) }), { allowSignalWrites: true }, ) show = signal(true) toggleShow() { this.show.update((show) => !show) } } ================================================ FILE: examples/angular/padding/src/app/row-virtualizer-padding.component.ts ================================================ import { ChangeDetectionStrategy, Component, ElementRef, effect, input, viewChild, viewChildren, } from '@angular/core' import { injectVirtualizer } from '@tanstack/angular-virtual' @Component({ standalone: true, selector: 'row-virtualizer-padding', changeDetection: ChangeDetectionStrategy.OnPush, template: `

Rows

@for (row of virtualizer.getVirtualItems(); track row.index) {
Row {{ row.index }}
}
`, styles: ` .scroll-container { height: 200px; width: 400px; overflow-y: auto; } `, }) export class RowVirtualizerPadding { rows = input.required() scrollElement = viewChild>('scrollElement') virtualItems = viewChildren>('virtualItem') #measureItems = effect( () => this.virtualItems().forEach((el) => { this.virtualizer.measureElement(el.nativeElement) }), { allowSignalWrites: true }, ) virtualizer = injectVirtualizer(() => ({ scrollElement: this.scrollElement(), count: this.rows().length, estimateSize: () => 50, paddingStart: 100, paddingEnd: 100, })) } ================================================ FILE: examples/angular/padding/src/index.html ================================================ @tanstack/virtualExampleAngularPadding ================================================ FILE: examples/angular/padding/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { AppComponent } from './app/app.component' bootstrapApplication(AppComponent).catch((err) => console.error(err)) ================================================ FILE: examples/angular/padding/src/styles.css ================================================ *, *:before, *:after { box-sizing: border-box; } html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .list { border: 1px solid #e6e4dc; max-width: 100%; } .list-item-even, .list-item-odd { display: flex; align-items: center; justify-content: center; } .list-item-even { background-color: #e6e4dc; } .list-item-odd { background-color: #fff; } button { border: 1px solid gray; } ================================================ FILE: examples/angular/padding/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/padding/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true } } ================================================ FILE: examples/angular/smooth-scroll/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:18" } ================================================ FILE: examples/angular/smooth-scroll/.gitignore ================================================ # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db ================================================ FILE: examples/angular/smooth-scroll/README.md ================================================ # @tanstack/virtualExampleAngularSmoothScroll This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: examples/angular/smooth-scroll/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "projects": { "@tanstack/virtual-example-angular-smooth-scroll": { "projectType": "application", "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/tanstack/virtual-example-angular-smooth-scroll", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico"], "styles": ["src/styles.css"], "scripts": [] }, "configurations": { "production": { "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "buildTarget": "@tanstack/virtual-example-angular-smooth-scroll:build:production" }, "development": { "buildTarget": "@tanstack/virtual-example-angular-smooth-scroll:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "buildTarget": "@tanstack/virtual-example-angular-smooth-scroll:build" } } } } } } ================================================ FILE: examples/angular/smooth-scroll/package.json ================================================ { "name": "@tanstack/virtual-example-angular-smooth-scroll", "private": true, "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "dependencies": { "@angular/animations": "^18.1.0", "@angular/common": "^18.1.0", "@angular/compiler": "^18.1.0", "@angular/core": "^18.1.0", "@angular/forms": "^18.1.0", "@angular/platform-browser": "^18.1.0", "@angular/platform-browser-dynamic": "^18.1.0", "@angular/router": "^18.1.0", "@tanstack/angular-virtual": "^4.0.11", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.1" }, "devDependencies": { "@angular-devkit/build-angular": "^18.1.0", "@angular/cli": "^18.1.0", "@angular/compiler-cli": "^18.1.0", "typescript": "5.4.5" } } ================================================ FILE: examples/angular/smooth-scroll/src/app/app.component.ts ================================================ import { ChangeDetectionStrategy, Component, ElementRef, signal, viewChild, } from '@angular/core' import { elementScroll, injectVirtualizer } from '@tanstack/angular-virtual' function easeInOutQuint(t: number) { return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t } @Component({ selector: 'app-root', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `

This smooth scroll example uses the scrollToFn to implement a custom scrolling function for the methods like scrollToIndex and scrollToOffset


@for (row of virtualizer.getVirtualItems(); track row.index) {
Row {{ row.index }}
}
`, styles: ` .scroll-container { height: 200px; width: 400px; overflow: auto; } `, }) export class AppComponent { scrollElement = viewChild>('scrollElement') scrollingTime = signal(0) virtualizer = injectVirtualizer(() => ({ scrollElement: this.scrollElement(), count: 10000, estimateSize: () => 35, overscan: 5, scrollToFn: (offset, options, instance) => { const duration = 1000 const start = this.scrollElement()!.nativeElement.scrollTop const startTime = Date.now() this.scrollingTime.set(startTime) const run = () => { if (this.scrollingTime() !== startTime) return const now = Date.now() const elapsed = now - startTime const progress = easeInOutQuint(Math.min(elapsed / duration, 1)) const interpolated = start + (offset - start) * progress if (elapsed < duration) { elementScroll(interpolated, options, instance) requestAnimationFrame(run) } else { elementScroll(interpolated, options, instance) } } requestAnimationFrame(run) }, })) randomIndex = signal(Math.floor(Math.random() * 10000)) scrollToRandomIndex() { this.virtualizer.scrollToIndex(this.randomIndex()) this.randomIndex.set(Math.floor(Math.random() * 10000)) } } ================================================ FILE: examples/angular/smooth-scroll/src/index.html ================================================ @tanstack/virtualExampleAngularSmoothScroll ================================================ FILE: examples/angular/smooth-scroll/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { AppComponent } from './app/app.component' bootstrapApplication(AppComponent).catch((err) => console.error(err)) ================================================ FILE: examples/angular/smooth-scroll/src/styles.css ================================================ *, *:before, *:after { box-sizing: border-box; } html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .list { border: 1px solid #e6e4dc; max-width: 100%; } .list-item-even, .list-item-odd { display: flex; align-items: center; justify-content: center; } .list-item-even { background-color: #e6e4dc; } .list-item-odd { background-color: #fff; } button { border: 1px solid gray; } ================================================ FILE: examples/angular/smooth-scroll/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/smooth-scroll/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true } } ================================================ FILE: examples/angular/sticky/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:18" } ================================================ FILE: examples/angular/sticky/.gitignore ================================================ # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db ================================================ FILE: examples/angular/sticky/README.md ================================================ # @tanstack/virtualExampleAngularSticky This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: examples/angular/sticky/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "projects": { "@tanstack/virtual-example-angular-sticky": { "projectType": "application", "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/tanstack/virtual-example-angular-sticky", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico"], "styles": ["src/styles.css"], "scripts": [] }, "configurations": { "production": { "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "buildTarget": "@tanstack/virtual-example-angular-sticky:build:production" }, "development": { "buildTarget": "@tanstack/virtual-example-angular-sticky:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "buildTarget": "@tanstack/virtual-example-angular-sticky:build" } } } } } } ================================================ FILE: examples/angular/sticky/package.json ================================================ { "name": "@tanstack/virtual-example-angular-sticky", "private": true, "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "dependencies": { "@angular/animations": "^18.1.0", "@angular/common": "^18.1.0", "@angular/compiler": "^18.1.0", "@angular/core": "^18.1.0", "@angular/forms": "^18.1.0", "@angular/platform-browser": "^18.1.0", "@angular/platform-browser-dynamic": "^18.1.0", "@angular/router": "^18.1.0", "@faker-js/faker": "^8.4.1", "@tanstack/angular-virtual": "^4.0.11", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.1" }, "devDependencies": { "@angular-devkit/build-angular": "^18.1.0", "@angular/cli": "^18.1.0", "@angular/compiler-cli": "^18.1.0", "typescript": "5.4.5" } } ================================================ FILE: examples/angular/sticky/src/app/app.component.ts ================================================ import { ChangeDetectionStrategy, Component, ElementRef, computed, viewChild, } from '@angular/core' import { faker } from '@faker-js/faker' import { injectVirtualizer, defaultRangeExtractor, } from '@tanstack/angular-virtual' const groupedNames: Record = {} Array.from({ length: 1000 }) .map(() => faker.person.firstName()) .sort() .forEach((name) => { const char = name[0] if (!groupedNames[char]) { groupedNames[char] = [] } groupedNames[char].push(name) }) const groups = Object.keys(groupedNames) const rows = groups.reduce( (acc: string[], k) => [...acc, k, ...groupedNames[k]], [], ) const stickyIndexes = groups.map((gn) => rows.findIndex((n) => n === gn)) const stickyIndexesSet = new Set(stickyIndexes) const reversedStickyIndexes = [...stickyIndexes].reverse() @Component({ selector: 'app-root', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
@for (row of virtualizer.getVirtualItems(); track row.index) {
{{ rows[row.index] }}
}
`, styles: ` .scroll-container { height: 300px; width: 400px; overflow: auto; } `, }) export class AppComponent { rows = rows isSticky = (index: number) => stickyIndexesSet.has(index) scrollElement = viewChild>('scrollElement') virtualizer = injectVirtualizer(() => ({ scrollElement: this.scrollElement(), count: this.rows.length, estimateSize: () => 50, rangeExtractor: (range) => { const next = new Set([ reversedStickyIndexes.find((index) => range.startIndex >= index)!, ...defaultRangeExtractor(range), ]) return [...next].sort((a, b) => a - b) }, })) activeStickyIndex = computed(() => { return this.virtualizer.getVirtualItems()[0]?.index }) isActiveSticky = (index: number) => this.activeStickyIndex() === index } ================================================ FILE: examples/angular/sticky/src/index.html ================================================ @tanstack/virtualExampleAngularSticky ================================================ FILE: examples/angular/sticky/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { AppComponent } from './app/app.component' bootstrapApplication(AppComponent).catch((err) => console.error(err)) ================================================ FILE: examples/angular/sticky/src/styles.css ================================================ *, *:before, *:after { box-sizing: border-box; } html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .list { border: 1px solid #e6e4dc; max-width: 100%; } .list-item-even, .list-item-odd { display: flex; align-items: center; justify-content: center; } .list-item-even { background-color: #e6e4dc; } .list-item-odd { background-color: #fff; } button { border: 1px solid gray; } ================================================ FILE: examples/angular/sticky/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/sticky/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true } } ================================================ FILE: examples/angular/table/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:18" } ================================================ FILE: examples/angular/table/.gitignore ================================================ # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db ================================================ FILE: examples/angular/table/README.md ================================================ # @tanstack/virtualExampleAngularTable This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: examples/angular/table/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "projects": { "@tanstack/virtual-example-angular-table": { "projectType": "application", "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/tanstack/virtual-example-angular-table", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico"], "styles": ["src/styles.css"], "scripts": [] }, "configurations": { "production": { "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "buildTarget": "@tanstack/virtual-example-angular-table:build:production" }, "development": { "buildTarget": "@tanstack/virtual-example-angular-table:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "buildTarget": "@tanstack/virtual-example-angular-table:build" } } } } } } ================================================ FILE: examples/angular/table/package.json ================================================ { "name": "@tanstack/virtual-example-angular-table", "private": true, "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "dependencies": { "@angular/animations": "^18.1.0", "@angular/common": "^18.1.0", "@angular/compiler": "^18.1.0", "@angular/core": "^18.1.0", "@angular/forms": "^18.1.0", "@angular/platform-browser": "^18.1.0", "@angular/platform-browser-dynamic": "^18.1.0", "@angular/router": "^18.1.0", "@faker-js/faker": "^8.4.1", "@tanstack/angular-table": "8.21.3", "@tanstack/angular-virtual": "^4.0.11", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.1" }, "devDependencies": { "@angular-devkit/build-angular": "^18.1.0", "@angular/cli": "^18.1.0", "@angular/compiler-cli": "^18.1.0", "typescript": "5.4.5" } } ================================================ FILE: examples/angular/table/src/app/app.component.ts ================================================ import { ChangeDetectionStrategy, Component, ElementRef, computed, signal, viewChild, } from '@angular/core' import { injectVirtualizer } from '@tanstack/angular-virtual' import { ColumnDef, createAngularTable, getCoreRowModel, getSortedRowModel, SortingState, FlexRenderDirective, SortDirection, } from '@tanstack/angular-table' import { makeData, type Person } from './make-data' @Component({ selector: 'app-root', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [FlexRenderDirective], template: `

For tables, the basis for the offset of the translate css function is from the row's initial position itself. Because of this, we need to calculate the translateY pixel count different and base it off the the index.


@for ( headerGroup of table.getHeaderGroups(); track headerGroup.id ) { @for (header of headerGroup.headers; track header.id) { } } @for ( virtualRow of virtualizer.getVirtualItems(); track data[virtualRow.index].id ) { @for ( cell of rows()[virtualRow.index].getVisibleCells(); track cell.id ) { } }
@if (!header.isPlaceholder) {
{{ headerText }} {{ getSortIcon(header.column.getIsSorted()) }}
}
{{ cellText }}
`, styles: ` .scroll-container { height: 600px; overflow: auto; } `, }) export class AppComponent { data = makeData(50_000) scrollElement = viewChild>('scrollElement') sorting = signal([]) sortIcons = { asc: '🔼', desc: '🔽' } getSortIcon(sorting: false | SortDirection) { return sorting ? this.sortIcons[sorting] : null } columns: ColumnDef[] = [ { accessorKey: 'id', header: 'ID', size: 60, }, { accessorKey: 'firstName', cell: (info) => info.getValue(), }, { accessorFn: (row) => row.lastName, id: 'lastName', cell: (info) => info.getValue(), header: 'Last Name', }, { accessorKey: 'age', header: () => 'Age', size: 50, }, { accessorKey: 'visits', header: 'Visits', size: 50, }, { accessorKey: 'status', header: 'Status', }, { accessorKey: 'progress', header: 'Profile Progress', size: 80, }, { accessorKey: 'createdAt', header: 'Created At', cell: (info) => info.getValue().toLocaleString(), }, ] table = createAngularTable(() => ({ data: this.data, columns: this.columns, state: { sorting: this.sorting(), }, onSortingChange: (updaterOrValue) => typeof updaterOrValue === 'function' ? this.sorting.update(updaterOrValue) : this.sorting.set(updaterOrValue), getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), debugTable: true, })) rows = computed(() => this.table.getRowModel().rows) virtualizer = injectVirtualizer(() => ({ scrollElement: this.scrollElement(), count: this.data.length, estimateSize: () => 34, overscan: 20, })) } ================================================ FILE: examples/angular/table/src/app/make-data.ts ================================================ import { faker } from '@faker-js/faker' export type Person = { id: number firstName: string lastName: string age: number visits: number progress: number status: 'relationship' | 'complicated' | 'single' createdAt: Date } const range = (len: number) => { const arr: number[] = [] for (let i = 0; i < len; i++) { arr.push(i) } return arr } const newPerson = (index: number): Person => { return { id: index + 1, firstName: faker.person.firstName(), lastName: faker.person.lastName(), age: faker.number.int(40), visits: faker.number.int(1000), progress: faker.number.int(100), createdAt: faker.datatype.datetime({ max: new Date().getTime() }), status: faker.helpers.shuffle([ 'relationship', 'complicated', 'single', ])[0]!, } } export function makeData(...lens: number[]) { const makeDataLevel = (depth = 0): Person[] => { const len = lens[depth]! return range(len).map((d): Person => { return { ...newPerson(d), } }) } return makeDataLevel() } ================================================ FILE: examples/angular/table/src/index.html ================================================ @tanstack/virtualExampleAngularTable ================================================ FILE: examples/angular/table/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { AppComponent } from './app/app.component' bootstrapApplication(AppComponent).catch((err) => console.error(err)) ================================================ FILE: examples/angular/table/src/styles.css ================================================ *, *:before, *:after { box-sizing: border-box; } html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .list { border: 1px solid #e6e4dc; max-width: 100%; } .list-item-even, .list-item-odd { display: flex; align-items: center; justify-content: center; } .list-item-even { background-color: #e6e4dc; } .list-item-odd { background-color: #fff; } button { border: 1px solid gray; } ================================================ FILE: examples/angular/table/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/table/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true } } ================================================ FILE: examples/angular/variable/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:18" } ================================================ FILE: examples/angular/variable/.gitignore ================================================ # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db ================================================ FILE: examples/angular/variable/README.md ================================================ # @tanstack/virtualExampleAngularVariable This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: examples/angular/variable/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "projects": { "@tanstack/virtual-example-angular-variable": { "projectType": "application", "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/tanstack/virtual-example-angular-variable", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico"], "styles": ["src/styles.css"], "scripts": [] }, "configurations": { "production": { "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "buildTarget": "@tanstack/virtual-example-angular-variable:build:production" }, "development": { "buildTarget": "@tanstack/virtual-example-angular-variable:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "buildTarget": "@tanstack/virtual-example-angular-variable:build" } } } } } } ================================================ FILE: examples/angular/variable/package.json ================================================ { "name": "@tanstack/virtual-example-angular-variable", "private": true, "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "dependencies": { "@angular/animations": "^18.1.0", "@angular/common": "^18.1.0", "@angular/compiler": "^18.1.0", "@angular/core": "^18.1.0", "@angular/forms": "^18.1.0", "@angular/platform-browser": "^18.1.0", "@angular/platform-browser-dynamic": "^18.1.0", "@angular/router": "^18.1.0", "@tanstack/angular-virtual": "^4.0.11", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.1" }, "devDependencies": { "@angular-devkit/build-angular": "^18.1.0", "@angular/cli": "^18.1.0", "@angular/compiler-cli": "^18.1.0", "typescript": "5.4.5" } } ================================================ FILE: examples/angular/variable/src/app/app.component.ts ================================================ import { ChangeDetectionStrategy, Component } from '@angular/core' import { ColumnVirtualizerVariable } from './column-virtualizer-variable.component' import { GridVirtualizerVariable } from './grid-virtualizer-variable.component' import { RowVirtualizerVariable } from './row-virtualizer-variable.component' @Component({ selector: 'app-root', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ ColumnVirtualizerVariable, GridVirtualizerVariable, RowVirtualizerVariable, ], template: `

These components are using variable sizes. This means that each element has a unique, but knowable dimension at render time.

`, styles: [], }) export class AppComponent { rows = new Array(10000) .fill(true) .map(() => 25 + Math.round(Math.random() * 100)) columns = new Array(10000) .fill(true) .map(() => 75 + Math.round(Math.random() * 100)) } ================================================ FILE: examples/angular/variable/src/app/column-virtualizer-variable.component.ts ================================================ import { ChangeDetectionStrategy, Component, ElementRef, input, viewChild, } from '@angular/core' import { injectVirtualizer } from '@tanstack/angular-virtual' @Component({ standalone: true, selector: 'column-virtualizer-variable', changeDetection: ChangeDetectionStrategy.OnPush, template: `

Columns

@for (col of virtualizer.getVirtualItems(); track col.index) {
Col {{ col.index }}
}
`, styles: ` .scroll-container { height: 100px; width: 400px; overflow: auto; } `, }) export class ColumnVirtualizerVariable { columns = input.required() scrollElement = viewChild>('scrollElement') virtualizer = injectVirtualizer(() => ({ horizontal: true, scrollElement: this.scrollElement(), count: 10000, estimateSize: (index) => this.columns()[index]!, overscan: 5, })) } ================================================ FILE: examples/angular/variable/src/app/grid-virtualizer-variable.component.ts ================================================ import { ChangeDetectionStrategy, Component, ElementRef, input, viewChild, } from '@angular/core' import { injectVirtualizer } from '@tanstack/angular-virtual' @Component({ standalone: true, selector: 'grid-virtualizer-variable', changeDetection: ChangeDetectionStrategy.OnPush, template: `

Grid

@for ( row of rowVirtualizer.getVirtualItems(); track row.index; let rowEven = $even ) { @for ( col of columnVirtualizer.getVirtualItems(); track col.index; let colEven = $even ) {
Cell {{ row.index }}, {{ col.index }}
} }
`, styles: ` .scroll-container { height: 500px; width: 500px; overflow: auto; } `, }) export class GridVirtualizerVariable { rows = input.required() columns = input.required() scrollElement = viewChild>('scrollElement') rowVirtualizer = injectVirtualizer(() => ({ scrollElement: this.scrollElement(), count: 10000, estimateSize: (index) => this.rows()[index]!, overscan: 5, })) columnVirtualizer = injectVirtualizer(() => ({ horizontal: true, scrollElement: this.scrollElement(), count: 10000, estimateSize: (index) => this.columns()[index]!, overscan: 5, })) } ================================================ FILE: examples/angular/variable/src/app/row-virtualizer-variable.component.ts ================================================ import { ChangeDetectionStrategy, Component, ElementRef, input, viewChild, } from '@angular/core' import { injectVirtualizer } from '@tanstack/angular-virtual' @Component({ standalone: true, selector: 'row-virtualizer-variable', changeDetection: ChangeDetectionStrategy.OnPush, template: `

Rows

@for (row of virtualizer.getVirtualItems(); track row.index) {
Row {{ row.index }}
}
`, styles: ` .scroll-container { height: 200px; width: 400px; overflow: auto; } `, }) export class RowVirtualizerVariable { rows = input.required() scrollElement = viewChild>('scrollElement') virtualizer = injectVirtualizer(() => ({ scrollElement: this.scrollElement(), count: this.rows().length, estimateSize: (index) => this.rows()[index]!, overscan: 5, })) } ================================================ FILE: examples/angular/variable/src/index.html ================================================ @tanstack/virtualExampleAngularVariable ================================================ FILE: examples/angular/variable/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { AppComponent } from './app/app.component' bootstrapApplication(AppComponent).catch((err) => console.error(err)) ================================================ FILE: examples/angular/variable/src/styles.css ================================================ *, *:before, *:after { box-sizing: border-box; } html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .list { border: 1px solid #e6e4dc; max-width: 100%; } .list-item-even, .list-item-odd { display: flex; align-items: center; justify-content: center; } .list-item-even { background-color: #e6e4dc; } .list-item-odd { background-color: #fff; } button { border: 1px solid gray; } ================================================ FILE: examples/angular/variable/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/variable/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true } } ================================================ FILE: examples/angular/window/.devcontainer/devcontainer.json ================================================ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:18" } ================================================ FILE: examples/angular/window/.gitignore ================================================ # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. # Compiled output /dist /tmp /out-tsc /bazel-out # Node /node_modules npm-debug.log yarn-error.log # IDEs and editors .idea/ .project .classpath .c9/ *.launch .settings/ *.sublime-workspace # Visual Studio Code .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history/* # Miscellaneous /.angular/cache .sass-cache/ /connect.lock /coverage /libpeerconnection.log testem.log /typings # System files .DS_Store Thumbs.db ================================================ FILE: examples/angular/window/README.md ================================================ # @tanstack/virtualExampleAngularWindow This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. ## Development server Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. ## Code scaffolding Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. ## Build Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. ## Running unit tests Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Running end-to-end tests Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: examples/angular/window/angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "cli": { "packageManager": "pnpm", "analytics": false, "cache": { "enabled": false } }, "projects": { "@tanstack/virtual-example-angular-window": { "projectType": "application", "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/tanstack/virtual-example-angular-window", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], "tsConfig": "tsconfig.app.json", "assets": ["src/favicon.ico"], "styles": ["src/styles.css"], "scripts": [] }, "configurations": { "production": { "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "buildTarget": "@tanstack/virtual-example-angular-window:build:production" }, "development": { "buildTarget": "@tanstack/virtual-example-angular-window:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "buildTarget": "@tanstack/virtual-example-angular-window:build" } } } } } } ================================================ FILE: examples/angular/window/package.json ================================================ { "name": "@tanstack/virtual-example-angular-window", "private": true, "type": "module", "scripts": { "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development" }, "dependencies": { "@angular/animations": "^18.1.0", "@angular/common": "^18.1.0", "@angular/compiler": "^18.1.0", "@angular/core": "^18.1.0", "@angular/forms": "^18.1.0", "@angular/platform-browser": "^18.1.0", "@angular/platform-browser-dynamic": "^18.1.0", "@angular/router": "^18.1.0", "@tanstack/angular-virtual": "^4.0.11", "rxjs": "^7.8.2", "tslib": "^2.8.1", "zone.js": "0.15.1" }, "devDependencies": { "@angular-devkit/build-angular": "^18.1.0", "@angular/cli": "^18.1.0", "@angular/compiler-cli": "^18.1.0", "typescript": "5.4.5" } } ================================================ FILE: examples/angular/window/src/app/app.component.ts ================================================ import { ChangeDetectionStrategy, Component, ElementRef, viewChild, } from '@angular/core' import { injectWindowVirtualizer } from '@tanstack/angular-virtual' @Component({ selector: 'app-root', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `

In many cases, when implementing a virtualizer with a window as the scrolling element, developers often find the need to specify a "scrollMargin." The scroll margin is a crucial setting that defines the space or gap between the start of the page and the edges of the list.

Window Scroller

@for (row of virtualizer.getVirtualItems(); track row.key) {
Row {{ row.index }}
}
`, styles: ` .scroll-container { height: 400px; width: 400px; overflow-y: auto; contain: 'strict'; } `, }) export class AppComponent { scrollElement = viewChild>('scrollElement') virtualizer = injectWindowVirtualizer(() => ({ count: 10000, estimateSize: () => 35, overscan: 5, scrollMargin: this.scrollElement()?.nativeElement.offsetTop, })) } ================================================ FILE: examples/angular/window/src/index.html ================================================ @tanstack/virtualExampleAngularWindow ================================================ FILE: examples/angular/window/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser' import { AppComponent } from './app/app.component' bootstrapApplication(AppComponent).catch((err) => console.error(err)) ================================================ FILE: examples/angular/window/src/styles.css ================================================ *, *:before, *:after { box-sizing: border-box; } html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .list { border: 1px solid #e6e4dc; max-width: 100%; } .list-item-even, .list-item-odd { display: flex; align-items: center; justify-content: center; } .list-item-even { background-color: #e6e4dc; } .list-item-odd { background-color: #fff; } button { border: 1px solid gray; } ================================================ FILE: examples/angular/window/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/app", "types": [] }, "files": ["src/main.ts"], "include": ["src/**/*.d.ts"] } ================================================ FILE: examples/angular/window/tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": ["ES2022", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true } } ================================================ FILE: examples/lit/dynamic/.gitignore ================================================ node_modules .DS_Store dist dist-ssr *.local ================================================ FILE: examples/lit/dynamic/README.md ================================================ # Example To run this example: - `npm install` or `npm` - `npm run start` or `npm run start` ================================================ FILE: examples/lit/dynamic/index.html ================================================
================================================ FILE: examples/lit/dynamic/package.json ================================================ { "name": "tanstack-lit-virtual-example-dynamic", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "serve": "vite preview" }, "dependencies": { "@faker-js/faker": "^8.4.1", "@tanstack/lit-virtual": "^3.13.24", "@tanstack/virtual-core": "^3.13.23", "lit": "^3.3.0" }, "devDependencies": { "@types/node": "^24.5.2", "typescript": "5.4.5", "vite": "^5.4.19" } } ================================================ FILE: examples/lit/dynamic/src/index.css ================================================ html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } ================================================ FILE: examples/lit/dynamic/src/main.ts ================================================ import { customElement, property } from 'lit/decorators.js' import { Ref, createRef, ref } from 'lit/directives/ref.js' import { html, LitElement } from 'lit' import { faker } from '@faker-js/faker' import { repeat } from 'lit/directives/repeat.js' import { VirtualizerController, WindowVirtualizerController, } from '@tanstack/lit-virtual' interface Column { key: string name: string width: number } function randomNumber(min: number, max: number) { return faker.number.int({ min, max }) } const sentences = new Array(10000) .fill(true) .map(() => faker.lorem.sentence(randomNumber(20, 70))) const generateColumns = (count: number) => { return new Array(count).fill(0).map((_, i) => { const key: string = i.toString() return { key, name: `Column ${i}`, width: randomNumber(75, 300), } }) } const generateData = (columns: Column[], count = 300) => { return new Array(count).fill(0).map((_, rowIndex) => columns.reduce((acc, _curr, colIndex) => { // simulate dynamic size cells const val = faker.lorem.lines(((rowIndex + colIndex) % 10) + 1) acc.push(val) return acc }, []), ) } @customElement('row-virtualizer-dynamic') class RowVirtualizerDynamic extends LitElement { private scrollElementRef: Ref = createRef() private virtualizerController: VirtualizerController constructor() { super() this.virtualizerController = new VirtualizerController(this, { getScrollElement: () => this.scrollElementRef.value, count: sentences.length, estimateSize: () => 45, }) } render() { const virtualizer = this.virtualizerController.getVirtualizer() const virtualRows = virtualizer.getVirtualItems() const count = sentences.length return html`
${repeat( virtualRows, (virtualRow) => virtualRow.key, (virtualRow) => html`
Row ${virtualRow.index}
${sentences[virtualRow.index]}
`, )}
` } } @customElement('column-virtualizer-dynamic') class ColumnVirtualizerDynamic extends LitElement { private scrollElementRef: Ref = createRef() private virtualizerController: VirtualizerController constructor() { super() this.virtualizerController = new VirtualizerController(this, { getScrollElement: () => this.scrollElementRef.value, count: sentences.length, estimateSize: () => 45, horizontal: true, }) } render() { const virtualizer = this.virtualizerController.getVirtualizer() const virtualColumns = virtualizer.getVirtualItems() return html`
${repeat( virtualColumns, (virtualColumn) => virtualColumn.key, (virtualColumn) => html`
Column ${virtualColumn.index}
${sentences[virtualColumn.index]}
`, )}
` } } @customElement('grid-virtualizer-dynamic') class GridVirtualizerDynamic extends LitElement { @property() private data: string[][] @property() private columns: Column[] private parentElementRef: Ref = createRef() private virtualizerController: WindowVirtualizerController private columnVirtualizerController: VirtualizerController< HTMLDivElement, Element > private getColumnWidth(index: number) { return this.columns[index].width } connectedCallback() { this.columnVirtualizerController = new VirtualizerController(this, { horizontal: true, count: this.columns.length, getScrollElement: () => this.parentElementRef.value, estimateSize: (index) => this.getColumnWidth(index), overscan: 5, }) this.virtualizerController = new WindowVirtualizerController(this, { count: this.data.length, estimateSize: () => 350, overscan: 5, }) super.connectedCallback() } render() { const virtualizer = this.virtualizerController.getVirtualizer() const columnVirtualizer = this.columnVirtualizerController.getVirtualizer() const columnItems = columnVirtualizer.getVirtualItems() const [before, after] = columnItems.length > 0 ? [ columnItems[0].start, columnVirtualizer.getTotalSize() - columnItems[columnItems.length - 1].end, ] : [0, 0] return html`
${repeat( virtualizer.getVirtualItems(), (row) => row.key, (row) => html`
${columnItems.map( (column) => html`
${row.index === 0 ? html`
${this.columns[column.index].name}
` : html`
${this.data[row.index][column.index]}
`}
`, )}
`, )}
` } } @customElement('my-app') export class MyApp extends LitElement { protected render() { const { pathname } = window.location return html`

These components are using dynamic sizes. This means that each element's exact dimensions are unknown when rendered. An estimated dimension is used as the initial measurement, then this measurement is readjusted on the fly as each element is rendered.

${(() => { switch (pathname) { case '/': return html`` case '/columns': return html`` case '/grid': { const columns = generateColumns(30) const data = generateData(columns) return html`` } default: return html`
Not found
` } })()}
` } } ================================================ FILE: examples/lit/dynamic/tsconfig.json ================================================ { "composite": true, "compilerOptions": { "outDir": "./build/types", "target": "ESNext", "module": "ESNext", "moduleResolution": "node", "experimentalDecorators": true, "useDefineForClassFields": false }, "files": ["src/main.ts"], "include": ["src"] } ================================================ FILE: examples/lit/dynamic/vite.config.js ================================================ import { defineConfig } from 'vite' // https://vitejs.dev/config/ export default defineConfig({ plugins: [], }) ================================================ FILE: examples/lit/fixed/.gitignore ================================================ node_modules .DS_Store dist dist-ssr *.local ================================================ FILE: examples/lit/fixed/README.md ================================================ # Example To run this example: - `npm install` or `npm` - `npm run start` or `npm run start` ================================================ FILE: examples/lit/fixed/index.html ================================================
================================================ FILE: examples/lit/fixed/package.json ================================================ { "name": "tanstack-lit-virtual-example-fixed", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "serve": "vite preview" }, "dependencies": { "@faker-js/faker": "^8.4.1", "@tanstack/lit-virtual": "^3.13.24", "@tanstack/virtual-core": "^3.13.23", "lit": "^3.3.0" }, "devDependencies": { "@types/node": "^24.5.2", "typescript": "5.4.5", "vite": "^5.4.19" } } ================================================ FILE: examples/lit/fixed/src/index.css ================================================ html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } ================================================ FILE: examples/lit/fixed/src/main.ts ================================================ import { customElement, property } from 'lit/decorators.js' import { Ref, createRef, ref } from 'lit/directives/ref.js' import { html, LitElement } from 'lit' import { faker } from '@faker-js/faker' import { repeat } from 'lit/directives/repeat.js' import { VirtualizerController, WindowVirtualizerController, } from '@tanstack/lit-virtual' interface Column { key: string name: string width: number } function randomNumber(min: number, max: number) { return faker.number.int({ min, max }) } const sentences = new Array(10000) .fill(true) .map(() => faker.lorem.sentence(randomNumber(20, 70))) const generateColumns = (count: number) => { return new Array(count).fill(0).map((_, i) => { const key: string = i.toString() return { key, name: `Column ${i}`, width: randomNumber(75, 300), } }) } const generateData = (columns: Column[], count = 300) => { return new Array(count).fill(0).map((_, rowIndex) => columns.reduce((acc, _curr, colIndex) => { // simulate dynamic size cells const val = faker.lorem.lines(((rowIndex + colIndex) % 10) + 1) acc.push(val) return acc }, []), ) } @customElement('row-virtualizer-fixed') class RowVirtualizerFixed extends LitElement { private scrollElementRef: Ref = createRef() private virtualizerController: VirtualizerController constructor() { super() this.virtualizerController = new VirtualizerController(this, { getScrollElement: () => this.scrollElementRef.value, count: 10000, estimateSize: () => 35, overscan: 5, }) } render() { const virtualizer = this.virtualizerController.getVirtualizer() const virtualRows = virtualizer.getVirtualItems() return html`
${repeat( virtualRows, (virtualRow) => virtualRow.key, (virtualRow) => html`
Row ${virtualRow.index}
`, )}
` } } @customElement('column-virtualizer-fixed') class ColumnVirtualizerFixed extends LitElement { private scrollElementRef: Ref = createRef() private virtualizerController: VirtualizerController constructor() { super() this.virtualizerController = new VirtualizerController(this, { getScrollElement: () => this.scrollElementRef.value, count: sentences.length, estimateSize: () => 100, horizontal: true, }) } render() { const virtualizer = this.virtualizerController.getVirtualizer() const virtualColumns = virtualizer.getVirtualItems() return html`
${repeat( virtualColumns, (virtualColumn) => virtualColumn.key, (virtualColumn) => html`
Column ${virtualColumn.index}
`, )}
` } } @customElement('grid-virtualizer-fixed') class GridVirtualizerFixed extends LitElement { private scrollElementRef: Ref = createRef() private rowVirtualizerController: VirtualizerController< HTMLDivElement, Element > private columnVirtualizerController: VirtualizerController< HTMLDivElement, Element > constructor() { super() this.rowVirtualizerController = new VirtualizerController(this, { getScrollElement: () => this.scrollElementRef.value, count: sentences.length, estimateSize: () => 35, overscan: 5, }) this.columnVirtualizerController = new VirtualizerController(this, { getScrollElement: () => this.scrollElementRef.value, count: sentences.length, estimateSize: () => 100, horizontal: true, overscan: 5, }) } render() { const rowVirtualizer = this.rowVirtualizerController.getVirtualizer() const columnVirtualizer = this.columnVirtualizerController.getVirtualizer() return html`
${repeat( rowVirtualizer.getVirtualItems(), (virtualRow) => virtualRow.key, (virtualRow) => repeat( columnVirtualizer.getVirtualItems(), (virtualColumn) => virtualColumn.key, (virtualColumn) => html`
Cell ${virtualRow.index}, ${virtualColumn.index}
`, ), )}
` } } @customElement('my-app') export class MyApp extends LitElement { protected render() { const { pathname } = window.location return html`

These components are using fixed sizes. This means that every element's dimensions are hard-coded to the same value and never change.



Rows



Columns



Grid



` } } ================================================ FILE: examples/lit/fixed/tsconfig.json ================================================ { "composite": true, "compilerOptions": { "outDir": "./build/types", "target": "ESNext", "module": "ESNext", "moduleResolution": "node", "experimentalDecorators": true, "useDefineForClassFields": false }, "files": ["src/main.ts"], "include": ["src"] } ================================================ FILE: examples/lit/fixed/vite.config.js ================================================ import { defineConfig } from 'vite' // https://vitejs.dev/config/ export default defineConfig({ plugins: [], }) ================================================ FILE: examples/react/dynamic/.gitignore ================================================ node_modules .DS_Store dist dist-ssr *.local ================================================ FILE: examples/react/dynamic/README.md ================================================ # Example To run this example: - `npm install` or `npm` - `npm run start` or `npm run start` ================================================ FILE: examples/react/dynamic/index.html ================================================
================================================ FILE: examples/react/dynamic/package.json ================================================ { "name": "tanstack-react-virtual-example-dynamic", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "serve": "vite preview" }, "dependencies": { "@faker-js/faker": "^8.4.1", "@tanstack/react-virtual": "^3.13.23", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { "@types/node": "^24.5.2", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.5.2", "typescript": "5.4.5", "vite": "^5.4.19" } } ================================================ FILE: examples/react/dynamic/src/index.css ================================================ *, *:before, *:after { box-sizing: border-box; } html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .List { border: 1px solid #e6e4dc; max-width: 100%; } .ListItemEven { background-color: #e6e4dc; } ================================================ FILE: examples/react/dynamic/src/main.tsx ================================================ import * as React from 'react' import { createRoot } from 'react-dom/client' import { faker } from '@faker-js/faker' import { useVirtualizer, useWindowVirtualizer } from '@tanstack/react-virtual' import './index.css' const randomNumber = (min: number, max: number) => faker.number.int({ min, max }) const sentences = new Array(10000) .fill(true) .map(() => faker.lorem.sentence(randomNumber(20, 70))) function RowVirtualizerDynamic() { const parentRef = React.useRef(null) const [enabled, setEnabled] = React.useState(true) const count = sentences.length const virtualizer = useVirtualizer({ count, getScrollElement: () => parentRef.current, estimateSize: () => 45, enabled, }) React.useEffect(() => { virtualizer.scrollToIndex(count - 1, { align: 'end' }) }, []) const items = virtualizer.getVirtualItems() return (

{items.map((virtualRow) => (
Row {virtualRow.index}
{sentences[virtualRow.index]}
))}
) } function ColumnVirtualizerDynamic() { const parentRef = React.useRef(null) const virtualizer = useVirtualizer({ horizontal: true, count: sentences.length, getScrollElement: () => parentRef.current, estimateSize: () => 45, }) return ( <>
{virtualizer.getVirtualItems().map((virtualColumn) => (
Column {virtualColumn.index}
{sentences[virtualColumn.index]}
))}
) } interface Column { key: string name: string width: number } function GridVirtualizerDynamic({ columns, data, }: { data: Array> columns: Array }) { const parentRef = React.useRef(null) const parentOffsetRef = React.useRef(0) React.useLayoutEffect(() => { parentOffsetRef.current = parentRef.current?.offsetTop ?? 0 }, []) const getColumnWidth = (index: number) => columns[index].width const virtualizer = useWindowVirtualizer({ count: data.length, estimateSize: () => 350, overscan: 5, scrollMargin: parentOffsetRef.current, }) const columnVirtualizer = useVirtualizer({ horizontal: true, count: columns.length, getScrollElement: () => parentRef.current, estimateSize: getColumnWidth, overscan: 5, }) const columnItems = columnVirtualizer.getVirtualItems() const [before, after] = columnItems.length > 0 ? [ columnItems[0].start, columnVirtualizer.getTotalSize() - columnItems[columnItems.length - 1].end, ] : [0, 0] return (
{virtualizer.getVirtualItems().map((row) => { return (
{columnItems.map((column) => { return (
{row.index === 0 ? (
{columns[column.index].name}
) : (
{data[row.index][column.index]}
)}
) })}
) })}
) } const generateColumns = (count: number) => { return new Array(count).fill(0).map((_, i) => { const key: string = i.toString() return { key, name: `Column ${i}`, width: randomNumber(75, 300), } }) } const generateData = (columns: Array, count = 300) => { return new Array(count).fill(0).map((_, rowIndex) => columns.reduce>((acc, _curr, colIndex) => { // simulate dynamic size cells const val = faker.lorem.lines(((rowIndex + colIndex) % 10) + 1) acc.push(val) return acc }, []), ) } function RowVirtualizerExperimental() { const parentRef = React.useRef(null) const innerRef = React.useRef(null) const rowRefsMap = React.useRef(new Map()) const [enabled, setEnabled] = React.useState(true) const count = sentences.length const virtualizer = useVirtualizer({ count, getScrollElement: () => parentRef.current, estimateSize: () => 45, enabled, onChange: (instance) => { innerRef.current!.style.height = `${instance.getTotalSize()}px` instance.getVirtualItems().forEach((virtualRow) => { const rowRef = rowRefsMap.current.get(virtualRow.index) if (!rowRef) return rowRef.style.transform = `translateY(${virtualRow.start}px)` }) }, }) const indexes = virtualizer.getVirtualIndexes() React.useEffect(() => { virtualizer.measure() }, []) return (

{indexes.map((index) => (
{ if (el) { virtualizer.measureElement(el) rowRefsMap.current.set(index, el) } }} className={index % 2 ? 'ListItemOdd' : 'ListItemEven'} >
Row {index}
{sentences[index]}
))}
) } function App() { const pathname = location.pathname return (

These components are using dynamic sizes. This means that each element's exact dimensions are unknown when rendered. An estimated dimension is used as the initial measurement, then this measurement is readjusted on the fly as each element is rendered.

{(() => { switch (pathname) { case '/': return case '/columns': return case '/grid': { const columns = generateColumns(30) const data = generateData(columns) return } case '/experimental': return default: return
Not found
} })()}

{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}
) } const container = document.getElementById('root')! const root = createRoot(container) const { StrictMode } = React root.render( , ) ================================================ FILE: examples/react/dynamic/tsconfig.json ================================================ { "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"] } ================================================ FILE: examples/react/dynamic/vite.config.js ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/react/fixed/.gitignore ================================================ node_modules .DS_Store dist dist-ssr *.local ================================================ FILE: examples/react/fixed/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run start` or `yarn start` ================================================ FILE: examples/react/fixed/index.html ================================================
================================================ FILE: examples/react/fixed/package.json ================================================ { "name": "tanstack-react-virtual-example-fixed", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "serve": "vite preview" }, "dependencies": { "@tanstack/react-virtual": "^3.13.23", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { "@types/node": "^24.5.2", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.5.2", "typescript": "5.4.5", "vite": "^5.4.19" } } ================================================ FILE: examples/react/fixed/src/index.css ================================================ 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; } ================================================ FILE: examples/react/fixed/src/main.tsx ================================================ import * as React from 'react' import * as ReactDOM from 'react-dom/client' import './index.css' import { useVirtualizer } from '@tanstack/react-virtual' function App() { return (

These components are using fixed sizes. This means that every element's dimensions are hard-coded to the same value and never change.



Rows



Columns



Grid



{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 RowVirtualizerFixed() { const parentRef = React.useRef(null) const rowVirtualizer = useVirtualizer({ count: 10000, getScrollElement: () => parentRef.current, estimateSize: () => 35, overscan: 5, }) return ( <>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
Row {virtualRow.index}
))}
) } function ColumnVirtualizerFixed() { const parentRef = React.useRef(null) const columnVirtualizer = useVirtualizer({ horizontal: true, count: 10000, getScrollElement: () => parentRef.current, estimateSize: () => 100, overscan: 5, }) return ( <>
{columnVirtualizer.getVirtualItems().map((virtualColumn) => (
Column {virtualColumn.index}
))}
) } function GridVirtualizerFixed() { const parentRef = React.useRef(null) const rowVirtualizer = useVirtualizer({ count: 10000, getScrollElement: () => parentRef.current, estimateSize: () => 35, overscan: 5, }) const columnVirtualizer = useVirtualizer({ horizontal: true, count: 10000, getScrollElement: () => parentRef.current, estimateSize: () => 100, overscan: 5, }) return ( <>
{rowVirtualizer.getVirtualItems().map((virtualRow) => ( {columnVirtualizer.getVirtualItems().map((virtualColumn) => (
Cell {virtualRow.index}, {virtualColumn.index}
))}
))}
) } // eslint-disable-next-line ReactDOM.createRoot(document.getElementById('root')!).render( , ) ================================================ FILE: examples/react/fixed/tsconfig.json ================================================ { "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"] } ================================================ FILE: examples/react/fixed/vite.config.js ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/react/infinite-scroll/.gitignore ================================================ node_modules .DS_Store dist dist-ssr *.local ================================================ FILE: examples/react/infinite-scroll/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run start` or `yarn start` ================================================ FILE: examples/react/infinite-scroll/index.html ================================================ Vite App
================================================ FILE: examples/react/infinite-scroll/package.json ================================================ { "name": "tanstack-react-virtual-example-infinite-scroll", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "serve": "vite preview --port 3001", "start": "vite" }, "dependencies": { "@tanstack/react-query": "^5.80.7", "@tanstack/react-virtual": "^3.13.23", "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" } } ================================================ FILE: examples/react/infinite-scroll/src/index.css ================================================ 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; } ================================================ FILE: examples/react/infinite-scroll/src/main.tsx ================================================ import React from 'react' import ReactDOM from 'react-dom' import { QueryClient, QueryClientProvider, useInfiniteQuery, } from '@tanstack/react-query' import './index.css' import { useVirtualizer } from '@tanstack/react-virtual' const queryClient = new QueryClient() async function fetchServerPage( limit: number, offset: number = 0, ): Promise<{ rows: Array; nextOffset: number }> { const rows = new Array(limit) .fill(0) .map((_, i) => `Async loaded row #${i + offset * limit}`) await new Promise((r) => setTimeout(r, 500)) return { rows, nextOffset: offset + 1 } } function App() { const { status, data, error, isFetching, isFetchingNextPage, fetchNextPage, hasNextPage, } = useInfiniteQuery({ queryKey: ['projects'], queryFn: (ctx) => fetchServerPage(10, ctx.pageParam), getNextPageParam: (lastGroup) => lastGroup.nextOffset, initialPageParam: 0, }) const allRows = data ? data.pages.flatMap((d) => d.rows) : [] const parentRef = React.useRef(null) const rowVirtualizer = useVirtualizer({ count: hasNextPage ? allRows.length + 1 : allRows.length, getScrollElement: () => parentRef.current, estimateSize: () => 100, overscan: 5, }) React.useEffect(() => { const [lastItem] = [...rowVirtualizer.getVirtualItems()].reverse() if (!lastItem) { return } if ( lastItem.index >= allRows.length - 1 && hasNextPage && !isFetchingNextPage ) { fetchNextPage() } }, [ hasNextPage, fetchNextPage, allRows.length, isFetchingNextPage, rowVirtualizer.getVirtualItems(), ]) return (

This infinite scroll example uses React Query's useInfiniteScroll hook to fetch infinite data from a posts endpoint and then a rowVirtualizer is used along with a loader-row placed at the bottom of the list to trigger the next page to load.



{status === 'pending' ? (

Loading...

) : status === 'error' ? ( Error: {error.message} ) : (
{rowVirtualizer.getVirtualItems().map((virtualRow) => { const isLoaderRow = virtualRow.index > allRows.length - 1 const post = allRows[virtualRow.index] return (
{isLoaderRow ? hasNextPage ? 'Loading more...' : 'Nothing more to load' : post}
) })}
)}
{isFetching && !isFetchingNextPage ? 'Background Updating...' : null}


{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}
) } ReactDOM.render( , document.getElementById('root'), ) ================================================ FILE: examples/react/infinite-scroll/tsconfig.json ================================================ { "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"] } ================================================ FILE: examples/react/infinite-scroll/vite.config.js ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/react/padding/.gitignore ================================================ node_modules .DS_Store dist dist-ssr *.local ================================================ FILE: examples/react/padding/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run start` or `yarn start` ================================================ FILE: examples/react/padding/index.html ================================================ Vite App
================================================ FILE: examples/react/padding/package.json ================================================ { "name": "tanstack-react-virtual-example-padding", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "serve": "vite preview --port 3001", "start": "vite" }, "dependencies": { "@tanstack/react-virtual": "^3.13.23", "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" } } ================================================ FILE: examples/react/padding/src/index.css ================================================ 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; } ================================================ FILE: examples/react/padding/src/main.tsx ================================================ import React from 'react' import ReactDOM from 'react-dom' import './index.css' import { useVirtualizer } from '@tanstack/react-virtual' const rows = new Array(10000) .fill(true) .map(() => 25 + Math.round(Math.random() * 100)) const columns = new Array(10000) .fill(true) .map(() => 75 + Math.round(Math.random() * 100)) function App() { return (

These components are using dynamic sizes. This means that each element's exact dimensions are unknown when rendered. An estimated dimension is used as the initial measurement, then this measurement is readjusted on the fly as each element is rendered.



Rows



Columns



Grid

) } function RowVirtualizerDynamic({ rows }: { rows: Array }) { const parentRef = React.useRef(null) const rowVirtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, paddingStart: 100, paddingEnd: 100, }) return ( <>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
Row {virtualRow.index}
))}
) } function ColumnVirtualizerDynamic({ columns }: { columns: Array }) { const parentRef = React.useRef(null) const columnVirtualizer = useVirtualizer({ horizontal: true, count: columns.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, paddingStart: 100, paddingEnd: 100, }) return ( <>
{columnVirtualizer.getVirtualItems().map((virtualColumn) => (
Column {virtualColumn.index}
))}
) } function GridVirtualizerDynamic({ rows, columns, }: { rows: Array columns: Array }) { const parentRef = React.useRef(null) const rowVirtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, paddingStart: 200, paddingEnd: 200, indexAttribute: 'data-row-index', }) const columnVirtualizer = useVirtualizer({ horizontal: true, count: columns.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, paddingStart: 200, paddingEnd: 200, indexAttribute: 'data-column-index', }) const [show, setShow] = React.useState(true) const halfWay = Math.floor(rows.length / 2) return ( <> {show ? (
{rowVirtualizer.getVirtualItems().map((virtualRow) => ( {columnVirtualizer.getVirtualItems().map((virtualColumn) => (
{ rowVirtualizer.measureElement(el) columnVirtualizer.measureElement(el) }} className={ virtualColumn.index % 2 ? virtualRow.index % 2 === 0 ? 'ListItemOdd' : 'ListItemEven' : virtualRow.index % 2 ? 'ListItemOdd' : 'ListItemEven' } style={{ position: 'absolute', top: 0, left: 0, width: `${columns[virtualColumn.index]}px`, height: `${rows[virtualRow.index]}px`, transform: `translateX(${virtualColumn.start}px) translateY(${virtualRow.start}px)`, }} > Cell {virtualRow.index}, {virtualColumn.index}
))}
))}
) : null}

{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} ) } ReactDOM.render( , document.getElementById('root'), ) ================================================ FILE: examples/react/padding/tsconfig.json ================================================ { "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"] } ================================================ FILE: examples/react/padding/vite.config.js ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/react/scroll-padding/.gitignore ================================================ node_modules .DS_Store dist dist-ssr *.local ================================================ FILE: examples/react/scroll-padding/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run start` or `yarn start` ================================================ FILE: examples/react/scroll-padding/index.html ================================================ Vite App
================================================ FILE: examples/react/scroll-padding/package.json ================================================ { "name": "tanstack-react-virtual-example-scroll-padding", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "serve": "vite preview --port 3001", "start": "vite" }, "dependencies": { "@react-hookz/web": "^25.1.1", "@tanstack/react-virtual": "^3.13.23", "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" } } ================================================ FILE: examples/react/scroll-padding/src/index.css ================================================ html { font-family: sans-serif; font-size: 14px; } .List table { background-color: #fff; border: 1px solid #e6e4dc; max-width: 100%; border-collapse: collapse; display: flex; flex-direction: column; align-items: stretch; position: relative; } .List thead { display: flex; flex-direction: column; background-color: #fff; position: sticky; top: 0; z-index: 1; } .List thead tr { height: 70px; } .List tr { display: flex; flex-direction: row; } .List td, .List th { display: flex; flex-direction: row; align-items: center; justify-content: center; width: 180px; } .ListItemEven { background-color: #e6e4dc; } ================================================ FILE: examples/react/scroll-padding/src/main.tsx ================================================ import React from 'react' import ReactDOM from 'react-dom' import './index.css' import { useMeasure } from '@react-hookz/web' import { useVirtualizer } from '@tanstack/react-virtual' function App() { const parentRef = React.useRef(null) const [theadSize, theadRef] = useMeasure() const rowVirtualizer = useVirtualizer({ count: 10000, getScrollElement: () => parentRef.current, estimateSize: React.useCallback(() => 35, []), overscan: 5, paddingStart: theadSize?.height ?? 0, scrollPaddingStart: theadSize?.height ?? 0, }) return ( <>


{rowVirtualizer.getVirtualItems().map((virtualRow) => ( ))}
Index Key
#{virtualRow.index} {virtualRow.key}
{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} ) } ReactDOM.render( , document.getElementById('root'), ) ================================================ FILE: examples/react/scroll-padding/tsconfig.json ================================================ { "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"] } ================================================ FILE: examples/react/scroll-padding/vite.config.js ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/react/smooth-scroll/.gitignore ================================================ node_modules .DS_Store dist dist-ssr *.local ================================================ FILE: examples/react/smooth-scroll/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run start` or `yarn start` ================================================ FILE: examples/react/smooth-scroll/index.html ================================================ Vite App
================================================ FILE: examples/react/smooth-scroll/package.json ================================================ { "name": "tanstack-react-virtual-example-smooth-scroll", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "serve": "vite preview --port 3001", "start": "vite" }, "dependencies": { "@tanstack/react-virtual": "^3.13.23", "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" } } ================================================ FILE: examples/react/smooth-scroll/src/index.css ================================================ 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; } ================================================ FILE: examples/react/smooth-scroll/src/main.tsx ================================================ import React from 'react' import ReactDOM from 'react-dom' import './index.css' import { elementScroll, useVirtualizer } from '@tanstack/react-virtual' import type { VirtualizerOptions } from '@tanstack/react-virtual' function easeInOutQuint(t: number) { return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t } function App() { const parentRef = React.useRef(null) const scrollingRef = React.useRef() const scrollToFn: VirtualizerOptions['scrollToFn'] = React.useCallback((offset, canSmooth, instance) => { const duration = 1000 const start = parentRef.current?.scrollTop || 0 const startTime = (scrollingRef.current = Date.now()) const run = () => { if (scrollingRef.current !== startTime) return const now = Date.now() const elapsed = now - startTime const progress = easeInOutQuint(Math.min(elapsed / duration, 1)) const interpolated = start + (offset - start) * progress if (elapsed < duration) { elementScroll(interpolated, canSmooth, instance) requestAnimationFrame(run) } else { elementScroll(interpolated, canSmooth, instance) } } requestAnimationFrame(run) }, []) const rowVirtualizer = useVirtualizer({ count: 10000, getScrollElement: () => parentRef.current, estimateSize: () => 35, overscan: 5, scrollToFn, }) const randomIndex = Math.floor(Math.random() * 10000) return (

This smooth scroll example uses the `scrollToFn` to implement a custom scrolling function for the methods like{' '} `scrollToIndex` and `scrollToOffset`





{rowVirtualizer.getVirtualItems().map((virtualRow) => (
Row {virtualRow.index}
))}


{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}
) } ReactDOM.render( , document.getElementById('root'), ) ================================================ FILE: examples/react/smooth-scroll/tsconfig.json ================================================ { "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"] } ================================================ FILE: examples/react/smooth-scroll/vite.config.js ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/react/sticky/.gitignore ================================================ node_modules .DS_Store dist dist-ssr *.local ================================================ FILE: examples/react/sticky/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run start` or `yarn start` ================================================ FILE: examples/react/sticky/index.html ================================================ Vite App
================================================ FILE: examples/react/sticky/package.json ================================================ { "name": "tanstack-react-virtual-example-sticky", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "serve": "vite preview --port 3001", "start": "vite" }, "dependencies": { "@faker-js/faker": "^8.4.1", "@tanstack/react-virtual": "^3.13.23", "lodash": "^4.17.21", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { "@types/lodash": "^4.17.17", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.5.2", "vite": "^5.4.19" } } ================================================ FILE: examples/react/sticky/src/index.css ================================================ 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; } ================================================ FILE: examples/react/sticky/src/main.tsx ================================================ import './index.css' import * as React from 'react' import ReactDOM from 'react-dom' import { faker } from '@faker-js/faker' import { findIndex, groupBy } from 'lodash' import { defaultRangeExtractor, useVirtualizer } from '@tanstack/react-virtual' import type { Range } from '@tanstack/react-virtual' const groupedNames = groupBy( Array.from({ length: 1000 }) .map(() => faker.person.firstName()) .sort(), (name) => name[0], ) const groups = Object.keys(groupedNames) const rows = groups.reduce>( (acc, k) => [...acc, k, ...groupedNames[k]], [], ) const App = () => { const parentRef = React.useRef(null) const activeStickyIndexRef = React.useRef(0) const stickyIndexes = React.useMemo( () => groups.map((gn) => findIndex(rows, (n) => n === gn)), [], ) const isSticky = (index: number) => stickyIndexes.includes(index) const isActiveSticky = (index: number) => activeStickyIndexRef.current === index const rowVirtualizer = useVirtualizer({ count: rows.length, estimateSize: () => 50, getScrollElement: () => parentRef.current, rangeExtractor: React.useCallback( (range: Range) => { activeStickyIndexRef.current = [...stickyIndexes] .reverse() .find((index) => range.startIndex >= index) ?? 0 const next = new Set([ activeStickyIndexRef.current, ...defaultRangeExtractor(range), ]) return [...next].sort((a, b) => a - b) }, [stickyIndexes], ), }) return (
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
{rows[virtualRow.index]}
))}
{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}
) } ReactDOM.render( , document.getElementById('root'), ) ================================================ FILE: examples/react/sticky/tsconfig.json ================================================ { "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"] } ================================================ FILE: examples/react/sticky/vite.config.js ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/react/table/.gitignore ================================================ node_modules .DS_Store dist dist-ssr *.local ================================================ FILE: examples/react/table/README.md ================================================ # Example To run this example: - `npm install` or `npm` - `npm run start` or `npm run start` ================================================ FILE: examples/react/table/index.html ================================================ Vite App
================================================ FILE: examples/react/table/package.json ================================================ { "name": "tanstack-react-virtual-example-table", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "serve": "vite preview --port 3001", "start": "vite" }, "dependencies": { "@faker-js/faker": "^8.4.1", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.23", "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" } } ================================================ FILE: examples/react/table/src/index.css ================================================ *, *:before, *:after { box-sizing: border-box; } html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .container { height: 600px; overflow: auto; } ================================================ FILE: examples/react/table/src/main.tsx ================================================ import * as React from 'react' import { createRoot } from 'react-dom/client' import { useVirtualizer } from '@tanstack/react-virtual' import { flexRender, getCoreRowModel, getSortedRowModel, useReactTable, } from '@tanstack/react-table' import { makeData } from './makeData' import type { ColumnDef, Row, SortingState } from '@tanstack/react-table' import type { Person } from './makeData' import './index.css' function ReactTableVirtualized() { const [sorting, setSorting] = React.useState([]) const columns = React.useMemo>>( () => [ { accessorKey: 'id', header: 'ID', size: 60, }, { accessorKey: 'firstName', cell: (info) => info.getValue(), }, { accessorFn: (row) => row.lastName, id: 'lastName', cell: (info) => info.getValue(), header: () => Last Name, }, { accessorKey: 'age', header: () => 'Age', size: 50, }, { accessorKey: 'visits', header: () => Visits, size: 50, }, { accessorKey: 'status', header: 'Status', }, { accessorKey: 'progress', header: 'Profile Progress', size: 80, }, { accessorKey: 'createdAt', header: 'Created At', cell: (info) => info.getValue().toLocaleString(), }, ], [], ) const [data, setData] = React.useState(() => makeData(50_000)) const table = useReactTable({ data, columns, state: { sorting, }, onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), debugTable: true, }) const { rows } = table.getRowModel() const parentRef = React.useRef(null) const virtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => parentRef.current, estimateSize: () => 34, overscan: 20, }) return (
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { return ( ) })} ))} {virtualizer.getVirtualItems().map((virtualRow, index) => { const row = rows[virtualRow.index] return ( {row.getVisibleCells().map((cell) => { return ( ) })} ) })}
{header.isPlaceholder ? null : (
{flexRender( header.column.columnDef.header, header.getContext(), )} {{ asc: ' 🔼', desc: ' 🔽', }[header.column.getIsSorted() as string] ?? null}
)}
{flexRender( cell.column.columnDef.cell, cell.getContext(), )}
) } function App() { return (

For tables, the basis for the offset of the translate css function is from the row's initial position itself. Because of this, we need to calculate the translateY pixel count differently and base it off the index.



{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}
) } const container = document.getElementById('root') const root = createRoot(container!) const { StrictMode } = React root.render( , ) ================================================ FILE: examples/react/table/src/makeData.ts ================================================ import { faker } from '@faker-js/faker' export type Person = { id: number firstName: string lastName: string age: number visits: number progress: number status: 'relationship' | 'complicated' | 'single' createdAt: Date } const range = (len: number) => { const arr: number[] = [] for (let i = 0; i < len; i++) { arr.push(i) } return arr } const newPerson = (index: number): Person => { return { id: index + 1, firstName: faker.person.firstName(), lastName: faker.person.lastName(), age: faker.number.int(40), visits: faker.number.int(1000), progress: faker.number.int(100), createdAt: faker.datatype.datetime({ max: new Date().getTime() }), status: faker.helpers.shuffle([ 'relationship', 'complicated', 'single', ])[0]!, } } export function makeData(...lens: number[]) { const makeDataLevel = (depth = 0): Person[] => { const len = lens[depth]! return range(len).map((d): Person => { return { ...newPerson(d), } }) } return makeDataLevel() } ================================================ FILE: examples/react/table/tsconfig.json ================================================ { "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"] } ================================================ FILE: examples/react/table/vite.config.js ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/react/variable/.gitignore ================================================ node_modules .DS_Store dist dist-ssr *.local ================================================ FILE: examples/react/variable/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run start` or `yarn start` ================================================ FILE: examples/react/variable/index.html ================================================ Vite App
================================================ FILE: examples/react/variable/package.json ================================================ { "name": "tanstack-react-virtual-example-variable", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "serve": "vite preview --port 3001", "start": "vite" }, "dependencies": { "@tanstack/react-virtual": "^3.13.23", "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" } } ================================================ FILE: examples/react/variable/src/index.css ================================================ 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; } ================================================ FILE: examples/react/variable/src/main.tsx ================================================ import React from 'react' import ReactDOM from 'react-dom' import './index.css' import { useVirtualizer } from '@tanstack/react-virtual' const rows = new Array(10000) .fill(true) .map(() => 25 + Math.round(Math.random() * 100)) const columns = new Array(10000) .fill(true) .map(() => 75 + Math.round(Math.random() * 100)) function App() { return (

These components are using variable sizes. This means that each element has a unique, but knowable dimension at render time.



Rows



Columns



Grid



Masonry (vertical)



Masonry (horizontal)



{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 RowVirtualizerVariable({ rows }: { rows: Array }) { const parentRef = React.useRef(null) const rowVirtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => parentRef.current, estimateSize: (i) => rows[i], overscan: 5, }) return ( <>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
Row {virtualRow.index}
))}
) } function ColumnVirtualizerVariable({ columns }: { columns: Array }) { const parentRef = React.useRef(null) const columnVirtualizer = useVirtualizer({ horizontal: true, count: columns.length, getScrollElement: () => parentRef.current, estimateSize: (i) => columns[i], overscan: 5, }) return ( <>
{columnVirtualizer.getVirtualItems().map((virtualColumn) => (
Column {virtualColumn.index}
))}
) } function GridVirtualizerVariable({ rows, columns, }: { rows: Array columns: Array }) { const parentRef = React.useRef(null) const rowVirtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => parentRef.current, estimateSize: (i) => rows[i], overscan: 5, }) const columnVirtualizer = useVirtualizer({ horizontal: true, count: columns.length, getScrollElement: () => parentRef.current, estimateSize: (i) => columns[i], overscan: 5, }) return ( <>
{rowVirtualizer.getVirtualItems().map((virtualRow) => ( {columnVirtualizer.getVirtualItems().map((virtualColumn) => (
Cell {virtualRow.index}, {virtualColumn.index}
))}
))}
) } function MasonryVerticalVirtualizerVariable({ rows }: { rows: Array }) { const parentRef = React.useRef(null) const rowVirtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => parentRef.current, estimateSize: (i) => rows[i], overscan: 5, lanes: 4, }) return ( <>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
Row {virtualRow.index}
))}
) } function MasonryHorizontalVirtualizerVariable({ rows, }: { rows: Array }) { const parentRef = React.useRef(null) const columnVirtualizer = useVirtualizer({ horizontal: true, count: columns.length, getScrollElement: () => parentRef.current, estimateSize: (i) => columns[i], overscan: 5, lanes: 4, }) return ( <>
{columnVirtualizer.getVirtualItems().map((virtualColumn) => (
Column {virtualColumn.index}
))}
) } ReactDOM.render( , document.getElementById('root'), ) ================================================ FILE: examples/react/variable/tsconfig.json ================================================ { "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"] } ================================================ FILE: examples/react/variable/vite.config.js ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/react/window/.gitignore ================================================ node_modules .DS_Store dist dist-ssr *.local ================================================ FILE: examples/react/window/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run start` or `yarn start` ================================================ FILE: examples/react/window/index.html ================================================
================================================ FILE: examples/react/window/package.json ================================================ { "name": "tanstack-react-virtual-example-window", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "serve": "vite preview" }, "dependencies": { "@tanstack/react-virtual": "^3.13.23", "react": "^18.3.1", "react-dom": "^18.3.1" }, "devDependencies": { "@types/node": "^24.5.2", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.5.2", "typescript": "5.4.5", "vite": "^5.4.19" } } ================================================ FILE: examples/react/window/src/index.css ================================================ 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; } ================================================ FILE: examples/react/window/src/main.tsx ================================================ import * as React from 'react' import * as ReactDOM from 'react-dom/client' import './index.css' import { useWindowVirtualizer } from '@tanstack/react-virtual' function Example() { const listRef = React.useRef(null) const listOffsetRef = React.useRef(0) React.useLayoutEffect(() => { listOffsetRef.current = listRef.current?.offsetTop ?? 0 }, []) const virtualizer = useWindowVirtualizer({ count: 10000, estimateSize: () => 35, overscan: 5, scrollMargin: listOffsetRef.current, }) return ( <>
{virtualizer.getVirtualItems().map((item) => (
Row {item.index}
))}
) } function App() { return (

In many cases, when implementing a virtualizer with a window as the scrolling element, developers often find the need to specify a "scrollMargin." The scroll margin is a crucial setting that defines the space or gap between the start of the page and the edges of the list.



Window scroller



{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}
) } ReactDOM.createRoot(document.getElementById('root')!).render( , ) ================================================ FILE: examples/react/window/tsconfig.json ================================================ { "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"] } ================================================ FILE: examples/react/window/vite.config.js ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }) ================================================ FILE: examples/svelte/dynamic/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/svelte/dynamic/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run dev` or `yarn dev` ================================================ FILE: examples/svelte/dynamic/index.html ================================================
================================================ FILE: examples/svelte/dynamic/package.json ================================================ { "name": "tanstack-svelte-virtual-example-dynamic", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "check": "svelte-check --tsconfig ./tsconfig.json" }, "dependencies": { "@faker-js/faker": "^8.4.1", "@tanstack/svelte-virtual": "^3.13.23" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.1.2", "@tsconfig/svelte": "^5.0.4", "svelte": "^4.2.20", "svelte-check": "^4.2.1", "tslib": "^2.8.1", "typescript": "5.4.5", "vite": "^5.4.19" } } ================================================ FILE: examples/svelte/dynamic/src/App.svelte ================================================

These components are using dynamic sizes. This means that each element's exact dimensions are unknown when rendered. An estimated dimension is used as the initial measurement, then this measurement is readjusted on the fly as each element is rendered.

{#if pathname === '/'} {:else if pathname === '/window-list'} {:else if pathname === '/columns'} {:else if pathname === '/grid'} {:else}

Not Found

{/if}
================================================ FILE: examples/svelte/dynamic/src/ColumnVirtualizerDynamic.svelte ================================================
{#each $virtualizer.getVirtualItems() as col, idx (col.index)}
Column {col.index}
{sentences[col.index]}
{/each}
================================================ FILE: examples/svelte/dynamic/src/GridVirtualizerDynamic.svelte ================================================
{#each $rowVirtualizer.getVirtualItems() as row (row.index)} {#each $columnVirtualizer.getVirtualItems() as col (col.index)}
Cell {row.index}, {col.index}
{/each} {/each}
================================================ FILE: examples/svelte/dynamic/src/RowVirtualizerDynamic.svelte ================================================

{#each items as row, idx (row.index)}
Row {row.index}
{sentences[row.index]}
{/each}
================================================ FILE: examples/svelte/dynamic/src/RowVirtualizerDynamicWindow.svelte ================================================
{#each items as row, idx (row.index)}
Row {row.index}
{sentences[row.index]}
{/each}
================================================ FILE: examples/svelte/dynamic/src/app.css ================================================ html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .list { border: 1px solid #e6e4dc; max-width: 100%; } .list-item-even, .list-item-odd { display: flex; align-items: center; justify-content: center; } .list-item-even { background-color: #e6e4dc; } button { border: 1px solid gray; } ================================================ FILE: examples/svelte/dynamic/src/main.ts ================================================ import './app.css' import App from './App.svelte' const app = new App({ target: document.getElementById('app')!, }) export default app ================================================ FILE: examples/svelte/dynamic/src/vite-env.d.ts ================================================ /// /// ================================================ FILE: examples/svelte/dynamic/svelte.config.js ================================================ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' export default { // Consult https://svelte.dev/docs#compile-time-svelte-preprocess // for more information about preprocessors preprocess: vitePreprocess(), } ================================================ FILE: examples/svelte/dynamic/tsconfig.json ================================================ { "extends": "@tsconfig/svelte/tsconfig.json", "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "resolveJsonModule": true, /** * Typecheck JS in `.svelte` and `.js` files by default. * Disable checkJs if you'd like to use dynamic types in JS. * Note that setting allowJs false does not prevent the use * of JS in `.svelte` files. */ "allowJs": true, "checkJs": true, "isolatedModules": true }, "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/svelte/dynamic/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler" }, "include": ["vite.config.ts"] } ================================================ FILE: examples/svelte/dynamic/vite.config.ts ================================================ import { defineConfig } from 'vite' import { svelte } from '@sveltejs/vite-plugin-svelte' // https://vitejs.dev/config/ export default defineConfig({ plugins: [svelte()], }) ================================================ FILE: examples/svelte/fixed/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/svelte/fixed/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run dev` or `yarn dev` ================================================ FILE: examples/svelte/fixed/index.html ================================================
================================================ FILE: examples/svelte/fixed/package.json ================================================ { "name": "tanstack-svelte-virtual-example-fixed", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "check": "svelte-check --tsconfig ./tsconfig.json" }, "dependencies": { "@tanstack/svelte-virtual": "^3.13.23" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.1.2", "@tsconfig/svelte": "^5.0.4", "svelte": "^4.2.20", "svelte-check": "^4.2.1", "tslib": "^2.8.1", "typescript": "5.4.5", "vite": "^5.4.19" } } ================================================ FILE: examples/svelte/fixed/src/App.svelte ================================================

These components are using fixed sizes. This means that every element's dimensions are hard-coded to the same value and never change.



Rows



Columns



Grid

================================================ FILE: examples/svelte/fixed/src/ColumnVirtualizerFixed.svelte ================================================
{#each $virtualizer.getVirtualItems() as col (col.index)}
Column {col.index}
{/each}
================================================ FILE: examples/svelte/fixed/src/GridVirtualizerFixed.svelte ================================================
{#each $rowVirtualizer.getVirtualItems() as row (row.index)} {#each $columnVirtualizer.getVirtualItems() as col (col.index)}
Cell {row.index}, {col.index}
{/each} {/each}
================================================ FILE: examples/svelte/fixed/src/RowVirtualizerFixed.svelte ================================================
{#each $virtualizer.getVirtualItems() as row (row.index)}
Row {row.index}
{/each}
================================================ FILE: examples/svelte/fixed/src/app.css ================================================ html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .list { border: 1px solid #e6e4dc; max-width: 100%; } .list-item-even, .list-item-odd { display: flex; align-items: center; justify-content: center; } .list-item-even { background-color: #e6e4dc; } button { border: 1px solid gray; } ================================================ FILE: examples/svelte/fixed/src/main.ts ================================================ import './app.css' import App from './App.svelte' const app = new App({ target: document.getElementById('app')!, }) export default app ================================================ FILE: examples/svelte/fixed/src/vite-env.d.ts ================================================ /// /// ================================================ FILE: examples/svelte/fixed/svelte.config.js ================================================ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' export default { // Consult https://svelte.dev/docs#compile-time-svelte-preprocess // for more information about preprocessors preprocess: vitePreprocess(), } ================================================ FILE: examples/svelte/fixed/tsconfig.json ================================================ { "extends": "@tsconfig/svelte/tsconfig.json", "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "resolveJsonModule": true, /** * Typecheck JS in `.svelte` and `.js` files by default. * Disable checkJs if you'd like to use dynamic types in JS. * Note that setting allowJs false does not prevent the use * of JS in `.svelte` files. */ "allowJs": true, "checkJs": true, "isolatedModules": true }, "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/svelte/fixed/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler" }, "include": ["vite.config.ts"] } ================================================ FILE: examples/svelte/fixed/vite.config.ts ================================================ import { defineConfig } from 'vite' import { svelte } from '@sveltejs/vite-plugin-svelte' // https://vitejs.dev/config/ export default defineConfig({ plugins: [svelte()], }) ================================================ FILE: examples/svelte/infinite-scroll/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/svelte/infinite-scroll/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run dev` or `yarn dev` ================================================ FILE: examples/svelte/infinite-scroll/index.html ================================================
================================================ FILE: examples/svelte/infinite-scroll/package.json ================================================ { "name": "tanstack-svelte-virtual-example-infinite-scroll", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "check": "svelte-check --tsconfig ./tsconfig.json" }, "dependencies": { "@tanstack/svelte-query": "^5.80.7", "@tanstack/svelte-virtual": "^3.13.23" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.1.2", "@tsconfig/svelte": "^5.0.4", "svelte": "^4.2.20", "svelte-check": "^4.2.1", "tslib": "^2.8.1", "typescript": "5.4.5", "vite": "^5.4.19" } } ================================================ FILE: examples/svelte/infinite-scroll/src/App.svelte ================================================ ================================================ FILE: examples/svelte/infinite-scroll/src/InfiniteRows.svelte ================================================

This infinite scroll example uses Svelte Query's createInfiniteQuery function to fetch infinite data from a posts endpoint and then a rowVirtualizer is used along with a loader-row placed at the bottom of the list to trigger the next page to load.



{#if $query.isLoading} Loading... {:else if $query.isError} Error: {$query.error.message} {:else if $query.isSuccess}
{#each $virtualizer.getVirtualItems() as row (row.index)}
{#if row.index > allRows.length - 1} {#if $query.hasNextPage} Loading more... {:else} Nothing more to load {/if} {:else} {allRows[row.index]} {/if}
{/each}
{/if} {#if $query.isFetching && !$query.isFetchingNextPage}

Background updating...

{/if}
================================================ FILE: examples/svelte/infinite-scroll/src/app.css ================================================ html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .list { border: 1px solid #e6e4dc; max-width: 100%; } .list-item-even, .list-item-odd { display: flex; align-items: center; justify-content: center; } .list-item-even { background-color: #e6e4dc; } button { border: 1px solid gray; } ================================================ FILE: examples/svelte/infinite-scroll/src/main.ts ================================================ import './app.css' import App from './App.svelte' const app = new App({ target: document.getElementById('app')!, }) export default app ================================================ FILE: examples/svelte/infinite-scroll/src/vite-env.d.ts ================================================ /// /// ================================================ FILE: examples/svelte/infinite-scroll/svelte.config.js ================================================ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' export default { // Consult https://svelte.dev/docs#compile-time-svelte-preprocess // for more information about preprocessors preprocess: vitePreprocess(), } ================================================ FILE: examples/svelte/infinite-scroll/tsconfig.json ================================================ { "extends": "@tsconfig/svelte/tsconfig.json", "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "resolveJsonModule": true, /** * Typecheck JS in `.svelte` and `.js` files by default. * Disable checkJs if you'd like to use dynamic types in JS. * Note that setting allowJs false does not prevent the use * of JS in `.svelte` files. */ "allowJs": true, "checkJs": true, "isolatedModules": true }, "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/svelte/infinite-scroll/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler" }, "include": ["vite.config.ts"] } ================================================ FILE: examples/svelte/infinite-scroll/vite.config.ts ================================================ import { defineConfig } from 'vite' import { svelte } from '@sveltejs/vite-plugin-svelte' // https://vitejs.dev/config/ export default defineConfig({ plugins: [svelte()], }) ================================================ FILE: examples/svelte/smooth-scroll/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/svelte/smooth-scroll/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run dev` or `yarn dev` ================================================ FILE: examples/svelte/smooth-scroll/index.html ================================================
================================================ FILE: examples/svelte/smooth-scroll/package.json ================================================ { "name": "tanstack-svelte-virtual-example-smooth-scroll", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "check": "svelte-check --tsconfig ./tsconfig.json" }, "dependencies": { "@faker-js/faker": "^8.4.1", "@tanstack/svelte-virtual": "^3.13.23" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.1.2", "@tsconfig/svelte": "^5.0.4", "svelte": "^4.2.20", "svelte-check": "^4.2.1", "tslib": "^2.8.1", "typescript": "5.4.5", "vite": "^5.4.19" } } ================================================ FILE: examples/svelte/smooth-scroll/src/App.svelte ================================================

This smooth scroll example uses the `scrollToFn` to implement a custom scrolling function for the methods like `scrollToIndex` and `scrollToOffset`





{#each $virtualizer.getVirtualItems() as row (row.index)}
Row {row.index}
{/each}
================================================ FILE: examples/svelte/smooth-scroll/src/app.css ================================================ html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .list { border: 1px solid #e6e4dc; max-width: 100%; } .list-item-even, .list-item-odd { display: flex; align-items: center; justify-content: center; } .list-item-even { background-color: #e6e4dc; } button { border: 1px solid gray; } ================================================ FILE: examples/svelte/smooth-scroll/src/main.ts ================================================ import './app.css' import App from './App.svelte' const app = new App({ target: document.getElementById('app')!, }) export default app ================================================ FILE: examples/svelte/smooth-scroll/src/vite-env.d.ts ================================================ /// /// ================================================ FILE: examples/svelte/smooth-scroll/svelte.config.js ================================================ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' export default { // Consult https://svelte.dev/docs#compile-time-svelte-preprocess // for more information about preprocessors preprocess: vitePreprocess(), } ================================================ FILE: examples/svelte/smooth-scroll/tsconfig.json ================================================ { "extends": "@tsconfig/svelte/tsconfig.json", "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "resolveJsonModule": true, /** * Typecheck JS in `.svelte` and `.js` files by default. * Disable checkJs if you'd like to use dynamic types in JS. * Note that setting allowJs false does not prevent the use * of JS in `.svelte` files. */ "allowJs": true, "checkJs": true, "isolatedModules": true }, "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/svelte/smooth-scroll/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler" }, "include": ["vite.config.ts"] } ================================================ FILE: examples/svelte/smooth-scroll/vite.config.ts ================================================ import { defineConfig } from 'vite' import { svelte } from '@sveltejs/vite-plugin-svelte' // https://vitejs.dev/config/ export default defineConfig({ plugins: [svelte()], }) ================================================ FILE: examples/svelte/sticky/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/svelte/sticky/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run dev` or `yarn dev` ================================================ FILE: examples/svelte/sticky/index.html ================================================
================================================ FILE: examples/svelte/sticky/package.json ================================================ { "name": "tanstack-svelte-virtual-example-sticky", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "check": "svelte-check --tsconfig ./tsconfig.json" }, "dependencies": { "@faker-js/faker": "^8.4.1", "@tanstack/svelte-virtual": "^3.13.23", "lodash": "^4.17.21" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.1.2", "@tsconfig/svelte": "^5.0.4", "svelte": "^4.2.20", "svelte-check": "^4.2.1", "tslib": "^2.8.1", "typescript": "5.4.5", "vite": "^5.4.19" } } ================================================ FILE: examples/svelte/sticky/src/App.svelte ================================================
{#each $virtualizer.getVirtualItems() as row (row.index)}
{rows[row.index]}
{/each}
================================================ FILE: examples/svelte/sticky/src/app.css ================================================ html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .list { border: 1px solid #e6e4dc; max-width: 100%; } .list-item-even, .list-item-odd { display: flex; align-items: center; justify-content: center; } .list-item-even { background-color: #e6e4dc; } button { border: 1px solid gray; } ================================================ FILE: examples/svelte/sticky/src/main.ts ================================================ import './app.css' import App from './App.svelte' const app = new App({ target: document.getElementById('app')!, }) export default app ================================================ FILE: examples/svelte/sticky/src/vite-env.d.ts ================================================ /// /// ================================================ FILE: examples/svelte/sticky/svelte.config.js ================================================ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' export default { // Consult https://svelte.dev/docs#compile-time-svelte-preprocess // for more information about preprocessors preprocess: vitePreprocess(), } ================================================ FILE: examples/svelte/sticky/tsconfig.json ================================================ { "extends": "@tsconfig/svelte/tsconfig.json", "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "resolveJsonModule": true, /** * Typecheck JS in `.svelte` and `.js` files by default. * Disable checkJs if you'd like to use dynamic types in JS. * Note that setting allowJs false does not prevent the use * of JS in `.svelte` files. */ "allowJs": true, "checkJs": true, "isolatedModules": true }, "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/svelte/sticky/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler" }, "include": ["vite.config.ts"] } ================================================ FILE: examples/svelte/sticky/vite.config.ts ================================================ import { defineConfig } from 'vite' import { svelte } from '@sveltejs/vite-plugin-svelte' // https://vitejs.dev/config/ export default defineConfig({ plugins: [svelte()], }) ================================================ FILE: examples/svelte/table/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/svelte/table/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run dev` or `yarn dev` ================================================ FILE: examples/svelte/table/index.html ================================================
================================================ FILE: examples/svelte/table/package.json ================================================ { "name": "tanstack-svelte-virtual-example-table", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "check": "svelte-check --tsconfig ./tsconfig.json" }, "dependencies": { "@faker-js/faker": "^8.4.1", "@tanstack/svelte-table": "^8.21.3", "@tanstack/svelte-virtual": "^3.13.23" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.1.2", "@tsconfig/svelte": "^5.0.4", "svelte": "^4.2.20", "svelte-check": "^4.2.1", "tslib": "^2.8.1", "typescript": "5.4.5", "vite": "^5.4.19" } } ================================================ FILE: examples/svelte/table/src/App.svelte ================================================

For tables, the basis for the offset of the translate css function is from the row's initial position itself. Because of this, we need to calculate the translateY pixel count differently and base it off the index.

{#each $table.getHeaderGroups() as headerGroup (headerGroup.id)} {#each headerGroup.headers as header (header.id)} {/each} {/each} {#each $virtualizer.getVirtualItems() as row, idx (row.index)} {#each rows[row.index].getVisibleCells() as cell (cell.id)} {/each} {/each}
{#if !header.isPlaceholder} {/if}
================================================ FILE: examples/svelte/table/src/app.css ================================================ html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .list { border: 1px solid #e6e4dc; max-width: 100%; } .list-item-even, .list-item-odd { display: flex; align-items: center; justify-content: center; } .list-item-even { background-color: #e6e4dc; } button { border: 1px solid gray; } ================================================ FILE: examples/svelte/table/src/main.ts ================================================ import './app.css' import App from './App.svelte' const app = new App({ target: document.getElementById('app')!, }) export default app ================================================ FILE: examples/svelte/table/src/makeData.ts ================================================ import { faker } from '@faker-js/faker' export type Person = { id: number firstName: string lastName: string age: number visits: number progress: number status: 'relationship' | 'complicated' | 'single' createdAt: Date } const range = (len: number) => { const arr: number[] = [] for (let i = 0; i < len; i++) { arr.push(i) } return arr } const newPerson = (index: number): Person => { return { id: index + 1, firstName: faker.person.firstName(), lastName: faker.person.lastName(), age: faker.number.int(40), visits: faker.number.int(1000), progress: faker.number.int(100), createdAt: faker.datatype.datetime({ max: new Date().getTime() }), status: faker.helpers.shuffle([ 'relationship', 'complicated', 'single', ])[0]!, } } export function makeData(...lens: number[]) { const makeDataLevel = (depth = 0): Person[] => { const len = lens[depth]! return range(len).map((d): Person => { return { ...newPerson(d), } }) } return makeDataLevel() } ================================================ FILE: examples/svelte/table/src/vite-env.d.ts ================================================ /// /// ================================================ FILE: examples/svelte/table/svelte.config.js ================================================ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' export default { // Consult https://svelte.dev/docs#compile-time-svelte-preprocess // for more information about preprocessors preprocess: vitePreprocess(), } ================================================ FILE: examples/svelte/table/tsconfig.json ================================================ { "extends": "@tsconfig/svelte/tsconfig.json", "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "resolveJsonModule": true, /** * Typecheck JS in `.svelte` and `.js` files by default. * Disable checkJs if you'd like to use dynamic types in JS. * Note that setting allowJs false does not prevent the use * of JS in `.svelte` files. */ "allowJs": true, "checkJs": true, "isolatedModules": true }, "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/svelte/table/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler" }, "include": ["vite.config.ts"] } ================================================ FILE: examples/svelte/table/vite.config.ts ================================================ import { defineConfig } from 'vite' import { svelte } from '@sveltejs/vite-plugin-svelte' // https://vitejs.dev/config/ export default defineConfig({ plugins: [svelte()], }) ================================================ FILE: examples/vue/dynamic/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/vue/dynamic/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run dev` or `yarn dev` ================================================ FILE: examples/vue/dynamic/index.html ================================================ Vite + Vue + TS
================================================ FILE: examples/vue/dynamic/package.json ================================================ { "name": "tanstack-vue-virtual-example-dynamic", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@faker-js/faker": "^8.4.1", "@tanstack/vue-virtual": "^3.13.23", "vue": "^3.5.16" }, "devDependencies": { "@codesandbox/vue-preview": "^0.1.1-alpha.16", "@vitejs/plugin-vue": "^5.2.4", "typescript": "5.4.5", "vite": "^5.4.19", "vue-tsc": "^2.2.10" } } ================================================ FILE: examples/vue/dynamic/src/App.vue ================================================ ================================================ FILE: examples/vue/dynamic/src/components/ColumnVirtualizerDynamic.vue ================================================ ================================================ FILE: examples/vue/dynamic/src/components/GridVirtualizerDynamic.vue ================================================ ================================================ FILE: examples/vue/dynamic/src/components/RowVirtualizerDynamic.vue ================================================ ================================================ FILE: examples/vue/dynamic/src/components/RowVirtualizerDynamicWindow.vue ================================================ ================================================ FILE: examples/vue/dynamic/src/components/utils.ts ================================================ import { faker } from '@faker-js/faker' interface Column { key: string name: string width: number } export const generateRandomNumber = (min: number, max: number) => faker.number.int({ min, max }) export const generateSentences = () => new Array(10000) .fill(true) .map(() => faker.lorem.sentence(generateRandomNumber(20, 70))) export const generateColumns = (count: number) => { return new Array(count).fill(0).map((_, i) => { const key: string = i.toString() return { key, name: `Column ${i}`, width: generateRandomNumber(75, 300), } }) } export const generateData = (columns: Column[], count = 300) => { return new Array(count).fill(0).map((_, rowIndex) => columns.reduce((acc, _curr, colIndex) => { // simulate dynamic size cells const val = faker.lorem.lines(((rowIndex + colIndex) % 10) + 1) acc.push(val) return acc }, []), ) } ================================================ FILE: examples/vue/dynamic/src/main.ts ================================================ import { createApp } from 'vue' import './style.css' import App from './App.vue' createApp(App).mount('#app') ================================================ FILE: examples/vue/dynamic/src/style.css ================================================ *, *:before, *:after { box-sizing: border-box; } html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .List { border: 1px solid #e6e4dc; max-width: 100%; } .ListItemEven { background-color: #e6e4dc; } ================================================ FILE: examples/vue/dynamic/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/vue/dynamic/tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Node", "strict": true, "jsx": "preserve", "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "lib": ["ESNext", "DOM"], "skipLibCheck": true, "noEmit": true, "noImplicitAny": false // Remove }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/vue/dynamic/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "module": "ESNext", "moduleResolution": "Node", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: examples/vue/dynamic/vite.config.ts ================================================ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], }) ================================================ FILE: examples/vue/fixed/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/vue/fixed/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run dev` or `yarn dev` ================================================ FILE: examples/vue/fixed/index.html ================================================ Vite + Vue + TS
================================================ FILE: examples/vue/fixed/package.json ================================================ { "name": "tanstack-vue-virtual-example-fixed", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@tanstack/vue-virtual": "^3.13.23", "vue": "^3.5.16" }, "devDependencies": { "@codesandbox/vue-preview": "^0.1.1-alpha.16", "@vitejs/plugin-vue": "^5.2.4", "typescript": "5.4.5", "vite": "^5.4.19", "vue-tsc": "^2.2.10" } } ================================================ FILE: examples/vue/fixed/src/App.vue ================================================ ================================================ FILE: examples/vue/fixed/src/components/ColumnVirtualizerFixed.vue ================================================ ================================================ FILE: examples/vue/fixed/src/components/GridVirtualizerFixed.vue ================================================ ================================================ FILE: examples/vue/fixed/src/components/RowVirtualizerFixed.vue ================================================ ================================================ FILE: examples/vue/fixed/src/main.ts ================================================ import { createApp } from 'vue' import './style.css' import App from './App.vue' createApp(App).mount('#app') ================================================ FILE: examples/vue/fixed/src/style.css ================================================ 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; } ================================================ FILE: examples/vue/fixed/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/vue/fixed/tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Node", "strict": true, "jsx": "preserve", "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "lib": ["ESNext", "DOM"], "skipLibCheck": true, "noEmit": true }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/vue/fixed/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "module": "ESNext", "moduleResolution": "Node", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: examples/vue/fixed/vite.config.ts ================================================ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], }) ================================================ FILE: examples/vue/infinite-scroll/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/vue/infinite-scroll/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run dev` or `yarn dev` ================================================ FILE: examples/vue/infinite-scroll/index.html ================================================ Vite + Vue + TS
================================================ FILE: examples/vue/infinite-scroll/package.json ================================================ { "name": "tanstack-vue-virtual-example-infinite-scroll", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@tanstack/vue-query": "^5.80.7", "@tanstack/vue-virtual": "^3.13.23", "vue": "^3.5.16" }, "devDependencies": { "@codesandbox/vue-preview": "^0.1.1-alpha.16", "@vitejs/plugin-vue": "^5.2.4", "typescript": "5.4.5", "vite": "^5.4.19", "vue-tsc": "^2.2.10" } } ================================================ FILE: examples/vue/infinite-scroll/src/App.vue ================================================ ================================================ FILE: examples/vue/infinite-scroll/src/main.ts ================================================ import { createApp } from 'vue' import { VueQueryPlugin } from '@tanstack/vue-query' import './style.css' import App from './App.vue' const app = createApp(App) app.use(VueQueryPlugin) app.mount('#app') ================================================ FILE: examples/vue/infinite-scroll/src/style.css ================================================ 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; } ================================================ FILE: examples/vue/infinite-scroll/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/vue/infinite-scroll/tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Node", "strict": true, "jsx": "preserve", "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "lib": ["ESNext", "DOM"], "skipLibCheck": true, "noEmit": true }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/vue/infinite-scroll/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "module": "ESNext", "moduleResolution": "Node", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: examples/vue/infinite-scroll/vite.config.ts ================================================ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], }) ================================================ FILE: examples/vue/padding/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/vue/padding/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run dev` or `yarn dev` ================================================ FILE: examples/vue/padding/index.html ================================================ Vite + Vue + TS
================================================ FILE: examples/vue/padding/package.json ================================================ { "name": "tanstack-vue-virtual-example-padding", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@tanstack/vue-virtual": "^3.13.23", "vue": "^3.5.16" }, "devDependencies": { "@codesandbox/vue-preview": "^0.1.1-alpha.16", "@vitejs/plugin-vue": "^5.2.4", "typescript": "5.4.5", "vite": "^5.4.19", "vue-tsc": "^2.2.10" } } ================================================ FILE: examples/vue/padding/src/App.vue ================================================ ================================================ FILE: examples/vue/padding/src/components/ColumnVirtualizerPadding.vue ================================================ ================================================ FILE: examples/vue/padding/src/components/GridVirtualizerPadding.vue ================================================ ================================================ FILE: examples/vue/padding/src/components/RowVirtualizerPadding.vue ================================================ ================================================ FILE: examples/vue/padding/src/main.ts ================================================ import { createApp } from 'vue' import './style.css' import App from './App.vue' createApp(App).mount('#app') ================================================ FILE: examples/vue/padding/src/style.css ================================================ 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; } ================================================ FILE: examples/vue/padding/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/vue/padding/tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Node", "strict": true, "jsx": "preserve", "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "lib": ["ESNext", "DOM"], "skipLibCheck": true, "noEmit": true }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/vue/padding/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "module": "ESNext", "moduleResolution": "Node", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: examples/vue/padding/vite.config.ts ================================================ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], }) ================================================ FILE: examples/vue/scroll-padding/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/vue/scroll-padding/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run dev` or `yarn dev` ================================================ FILE: examples/vue/scroll-padding/index.html ================================================ Vite + Vue + TS
================================================ FILE: examples/vue/scroll-padding/package.json ================================================ { "name": "tanstack-vue-virtual-example-scroll-padding", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@tanstack/vue-virtual": "^3.13.23", "@vueuse/core": "^12.8.2", "vue": "^3.5.16" }, "devDependencies": { "@codesandbox/vue-preview": "^0.1.1-alpha.16", "@vitejs/plugin-vue": "^5.2.4", "typescript": "5.4.5", "vite": "^5.4.19", "vue-tsc": "^2.2.10" } } ================================================ FILE: examples/vue/scroll-padding/src/App.vue ================================================ ================================================ FILE: examples/vue/scroll-padding/src/main.ts ================================================ import { createApp } from 'vue' import './style.css' import App from './App.vue' createApp(App).mount('#app') ================================================ FILE: examples/vue/scroll-padding/src/style.css ================================================ html { font-family: sans-serif; font-size: 14px; } .List table { background-color: #fff; border: 1px solid #e6e4dc; max-width: 100%; border-collapse: collapse; display: flex; flex-direction: column; align-items: stretch; position: relative; } .List thead { display: flex; flex-direction: column; background-color: #fff; position: sticky; top: 0; z-index: 1; } .List thead tr { height: 70px; } .List tr { display: flex; flex-direction: row; } .List td, .List th { display: flex; flex-direction: row; align-items: center; justify-content: center; width: 180px; } .ListItemEven { background-color: #e6e4dc; } ================================================ FILE: examples/vue/scroll-padding/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/vue/scroll-padding/tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Node", "strict": true, "jsx": "preserve", "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "lib": ["ESNext", "DOM"], "skipLibCheck": true, "noEmit": true }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/vue/scroll-padding/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "module": "ESNext", "moduleResolution": "Node", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: examples/vue/scroll-padding/vite.config.ts ================================================ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], }) ================================================ FILE: examples/vue/smooth-scroll/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/vue/smooth-scroll/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run dev` or `yarn dev` ================================================ FILE: examples/vue/smooth-scroll/index.html ================================================ Vite + Vue + TS
================================================ FILE: examples/vue/smooth-scroll/package.json ================================================ { "name": "tanstack-vue-virtual-example-smooth-scroll", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@tanstack/vue-virtual": "^3.13.23", "vue": "^3.5.16" }, "devDependencies": { "@codesandbox/vue-preview": "^0.1.1-alpha.16", "@vitejs/plugin-vue": "^5.2.4", "typescript": "5.4.5", "vite": "^5.4.19", "vue-tsc": "^2.2.10" } } ================================================ FILE: examples/vue/smooth-scroll/src/App.vue ================================================ ================================================ FILE: examples/vue/smooth-scroll/src/main.ts ================================================ import { createApp } from 'vue' import './style.css' import App from './App.vue' createApp(App).mount('#app') ================================================ FILE: examples/vue/smooth-scroll/src/style.css ================================================ 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; } ================================================ FILE: examples/vue/smooth-scroll/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/vue/smooth-scroll/tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Node", "strict": true, "jsx": "preserve", "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "lib": ["ESNext", "DOM"], "skipLibCheck": true, "noEmit": true }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/vue/smooth-scroll/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "module": "ESNext", "moduleResolution": "Node", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: examples/vue/smooth-scroll/vite.config.ts ================================================ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], }) ================================================ FILE: examples/vue/sticky/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/vue/sticky/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run dev` or `yarn dev` ================================================ FILE: examples/vue/sticky/index.html ================================================ Vite + Vue + TS
================================================ FILE: examples/vue/sticky/package.json ================================================ { "name": "tanstack-vue-virtual-example-sticky", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@faker-js/faker": "^8.4.1", "@tanstack/vue-virtual": "^3.13.23", "lodash": "^4.17.21", "vue": "^3.5.16" }, "devDependencies": { "@codesandbox/vue-preview": "^0.1.1-alpha.16", "@types/lodash": "^4.17.17", "@vitejs/plugin-vue": "^5.2.4", "typescript": "5.4.5", "vite": "^5.4.19", "vue-tsc": "^2.2.10" } } ================================================ FILE: examples/vue/sticky/src/App.vue ================================================ ================================================ FILE: examples/vue/sticky/src/main.ts ================================================ import { createApp } from 'vue' import './style.css' import App from './App.vue' createApp(App).mount('#app') ================================================ FILE: examples/vue/sticky/src/style.css ================================================ 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; } ================================================ FILE: examples/vue/sticky/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/vue/sticky/tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Node", "strict": true, "jsx": "preserve", "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "lib": ["ESNext", "DOM"], "skipLibCheck": true, "noEmit": true }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/vue/sticky/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "module": "ESNext", "moduleResolution": "Node", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: examples/vue/sticky/vite.config.ts ================================================ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], }) ================================================ FILE: examples/vue/table/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/vue/table/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run dev` or `yarn dev` ================================================ FILE: examples/vue/table/index.html ================================================ Vite + Vue + TS
================================================ FILE: examples/vue/table/package.json ================================================ { "name": "tanstack-vue-virtual-example-table", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@faker-js/faker": "^8.4.1", "@tanstack/vue-table": "^8.21.3", "@tanstack/vue-virtual": "^3.13.23", "vue": "^3.5.16" }, "devDependencies": { "@codesandbox/vue-preview": "^0.1.1-alpha.16", "@vitejs/plugin-vue": "^5.2.4", "typescript": "5.4.5", "vite": "^5.4.19", "vue-tsc": "^2.2.10" } } ================================================ FILE: examples/vue/table/src/App.vue ================================================ ================================================ FILE: examples/vue/table/src/main.ts ================================================ import { createApp } from 'vue' import './style.css' import App from './App.vue' createApp(App).mount('#app') ================================================ FILE: examples/vue/table/src/makeData.ts ================================================ import { faker } from '@faker-js/faker' export type Person = { id: number firstName: string lastName: string age: number visits: number progress: number status: 'relationship' | 'complicated' | 'single' createdAt: Date } const range = (len: number) => { const arr: number[] = [] for (let i = 0; i < len; i++) { arr.push(i) } return arr } const newPerson = (index: number): Person => { return { id: index + 1, firstName: faker.person.firstName(), lastName: faker.person.lastName(), age: faker.number.int(40), visits: faker.number.int(1000), progress: faker.number.int(100), createdAt: faker.datatype.datetime({ max: new Date().getTime() }), status: faker.helpers.shuffle([ 'relationship', 'complicated', 'single', ])[0]!, } } export function makeData(...lens: number[]) { const makeDataLevel = (depth = 0): Person[] => { const len = lens[depth]! return range(len).map((d): Person => { return { ...newPerson(d), } }) } return makeDataLevel() } ================================================ FILE: examples/vue/table/src/style.css ================================================ *, *:before, *:after { box-sizing: border-box; } html { font-family: sans-serif; font-size: 14px; } body { padding: 1rem; } .container { height: 600px; overflow: auto; } .cursor-pointer { cursor: pointer; } .select-none { user-select: none; } .text-left { text-align: left; } ================================================ FILE: examples/vue/table/src/vite-env.d.ts ================================================ /// ================================================ FILE: examples/vue/table/tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Node", "strict": true, "jsx": "preserve", "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "lib": ["ESNext", "DOM"], "skipLibCheck": true, "noEmit": true }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/vue/table/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "module": "ESNext", "moduleResolution": "Node", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: examples/vue/table/vite.config.ts ================================================ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], }) ================================================ FILE: examples/vue/variable/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/vue/variable/README.md ================================================ # Example To run this example: - `npm install` or `yarn` - `npm run dev` or `yarn dev` ================================================ FILE: examples/vue/variable/index.html ================================================ Vite + Vue + TS
================================================ FILE: examples/vue/variable/package.json ================================================ { "name": "tanstack-vue-virtual-example-variable", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "@tanstack/vue-virtual": "^3.13.23", "vue": "^3.5.16" }, "devDependencies": { "@codesandbox/vue-preview": "^0.1.1-alpha.16", "@vitejs/plugin-vue": "^5.2.4", "typescript": "5.4.5", "vite": "^5.4.19", "vue-tsc": "^2.2.10" } } ================================================ FILE: examples/vue/variable/src/App.vue ================================================ ================================================ FILE: examples/vue/variable/src/components/ColumnVirtualizerVariable.vue ================================================ ================================================ FILE: examples/vue/variable/src/components/GridVirtualizerVariable.vue ================================================ ================================================ FILE: examples/vue/variable/src/components/MasonryHorizontalVirtualizerVariable.vue ================================================ ================================================ FILE: examples/vue/variable/src/components/MasonryVerticalVirtualizerVariable.vue ================================================ ================================================ FILE: examples/vue/variable/src/components/RowVirtualizerVariable.vue ================================================ ================================================ FILE: examples/vue/variable/src/main.ts ================================================ import { createApp } from 'vue' import './style.css' import App from './App.vue' createApp(App).mount('#app') ================================================ FILE: examples/vue/variable/src/style.css ================================================ 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; } ================================================ FILE: examples/vue/variable/src/vite-env.d.ts ================================================ // ================================================ FILE: examples/vue/variable/tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Node", "strict": true, "jsx": "preserve", "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "lib": ["ESNext", "DOM"], "skipLibCheck": true, "noEmit": true }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: examples/vue/variable/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "module": "ESNext", "moduleResolution": "Node", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: examples/vue/variable/vite.config.ts ================================================ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], }) ================================================ FILE: knip.json ================================================ { "$schema": "https://unpkg.com/knip@5/schema.json", "ignoreWorkspaces": ["examples/**", "packages/react-virtual/e2e/**"] } ================================================ FILE: nx.json ================================================ { "$schema": "./node_modules/nx/schemas/nx-schema.json", "defaultBase": "main", "nxCloudId": "65d1330edb0c1e7def5cb068", "useInferencePlugins": false, "parallel": 5, "tui": { "enabled": false }, "namedInputs": { "sharedGlobals": [ "{workspaceRoot}/.nvmrc", "{workspaceRoot}/package.json", "{workspaceRoot}/tsconfig.json" ], "default": [ "sharedGlobals", "{projectRoot}/**/*", "!{projectRoot}/**/*.md" ], "production": [ "default", "!{projectRoot}/tests/**/*", "!{projectRoot}/eslint.config.js" ] }, "targetDefaults": { "test:docs": { "cache": true, "inputs": ["{workspaceRoot}/docs/**/*"] }, "test:knip": { "cache": true, "inputs": ["{workspaceRoot}/**/*"] }, "test:sherif": { "cache": true, "inputs": ["{workspaceRoot}/**/package.json"] }, "test:eslint": { "cache": true, "dependsOn": ["^build"], "inputs": ["default", "^production", "{workspaceRoot}/eslint.config.js"] }, "test:lib": { "cache": true, "dependsOn": ["^build"], "inputs": ["default", "^production"], "outputs": ["{projectRoot}/coverage"] }, "test:types": { "cache": true, "dependsOn": ["^build"], "inputs": ["default", "^production"] }, "build": { "cache": true, "dependsOn": ["^build"], "inputs": ["production", "^production"], "outputs": ["{projectRoot}/build", "{projectRoot}/dist"] }, "test:build": { "cache": true, "dependsOn": ["build"], "inputs": ["production"] } } } ================================================ FILE: package.json ================================================ { "name": "root", "private": true, "repository": { "type": "git", "url": "git+https://github.com/TanStack/virtual.git" }, "packageManager": "pnpm@10.24.0", "type": "module", "scripts": { "clean": "pnpm --filter \"./packages/**\" run clean", "test": "pnpm run test:ci", "test:pr": "nx affected --targets=test:sherif,test:knip,test:docs,test:eslint,test:lib,test:e2e,test:types,test:build,build", "test:ci": "nx run-many --targets=test:sherif,test:knip,test:docs,test:eslint,test:lib,test:e2e,test:types,test:build,build", "test:eslint": "nx affected --target=test:eslint", "test:sherif": "sherif", "test:lib": "nx affected --target=test:lib --exclude=examples/**", "test:lib:dev": "pnpm run test:lib && nx watch --all -- pnpm run test:lib", "test:build": "nx affected --target=test:build --exclude=examples/**", "test:types": "nx affected --target=test:types --exclude=examples/**", "test:e2e": "nx affected --target=test:e2e --exclude=examples/**", "test:knip": "knip", "test:docs": "node scripts/verify-links.ts", "build": "nx affected --target=build --exclude=examples/**", "build:all": "nx run-many --target=build --exclude=examples/**", "watch": "pnpm run build:all && nx watch --all -- pnpm run build:all", "dev": "pnpm run watch", "format": "prettier --experimental-cli --ignore-unknown '**/*' --write", "changeset": "changeset", "changeset:version": "changeset version && pnpm install --no-frozen-lockfile && pnpm format", "changeset:publish": "changeset publish" }, "nx": { "includedScripts": [ "test:docs", "test:knip", "test:sherif" ] }, "devDependencies": { "@changesets/cli": "^2.30.0", "@playwright/test": "^1.53.1", "@svitejs/changesets-changelog-github-compact": "^1.2.0", "@tanstack/eslint-config": "0.3.4", "@tanstack/vite-config": "0.3.0", "@testing-library/jest-dom": "^6.6.3", "@types/node": "^24.5.2", "eslint": "^9.36.0", "jsdom": "^27.0.0", "knip": "^5.63.1", "markdown-link-extractor": "^4.0.2", "nx": "22.1.3", "premove": "^4.0.0", "prettier": "^3.7.4", "prettier-plugin-svelte": "^3.4.0", "publint": "^0.3.15", "sherif": "^1.9.0", "tinyglobby": "^0.2.15", "typescript": "5.4.5", "vite": "^5.4.19", "vitest": "^2.1.9" } } ================================================ FILE: packages/angular-virtual/CHANGELOG.md ================================================ # @tanstack/angular-virtual ## 4.0.11 ### Patch Changes - Updated dependencies [[`7ece2d5`](https://github.com/TanStack/virtual/commit/7ece2d5d4249b7e703c68ac497ae5545c54e7c67)]: - @tanstack/virtual-core@3.13.23 ## 4.0.10 ### Patch Changes - Updated dependencies [[`54d771a`](https://github.com/TanStack/virtual/commit/54d771a7d4c74f6968e8132b5a85f3e04682376a), [`d3416c3`](https://github.com/TanStack/virtual/commit/d3416c386c6446957f413db2eef3211f5fdf3b5f)]: - @tanstack/virtual-core@3.13.22 ## 4.0.9 ### Patch Changes - Updated dependencies [[`be89e29`](https://github.com/TanStack/virtual/commit/be89e293ea01654df6334dc6473b65eebed13e51)]: - @tanstack/virtual-core@3.13.21 ## 4.0.8 ### Patch Changes - Updated dependencies [[`ff83e94`](https://github.com/TanStack/virtual/commit/ff83e949408ba8a714436fa10cafc3725a56274b)]: - @tanstack/virtual-core@3.13.20 ## 4.0.7 ### Patch Changes - Updated dependencies [[`843109c`](https://github.com/TanStack/virtual/commit/843109c5bf780591a762f9767f3808fd15e3f94e)]: - @tanstack/virtual-core@3.13.19 ## 4.0.6 ### Patch Changes - Updated dependencies [[`9067574`](https://github.com/TanStack/virtual/commit/9067574f1a0178d30e27bcac70853bdcbf437fec)]: - @tanstack/virtual-core@3.13.18 ## 4.0.5 ### Patch Changes - Updated dependencies [[`21d9a46`](https://github.com/TanStack/virtual/commit/21d9a46eac034cb4299872891694965bceed526d)]: - @tanstack/virtual-core@3.13.17 ## 4.0.4 ### Patch Changes - Updated dependencies [[`db6df21`](https://github.com/TanStack/virtual/commit/db6df212ed83dd7e4eb6450d1340c95475667b7b)]: - @tanstack/virtual-core@3.13.16 ## 4.0.3 ### Patch Changes - Updated dependencies [[`5a273bf`](https://github.com/TanStack/virtual/commit/5a273bf0c0bc0255ca172929f021c3b6e50cb69d)]: - @tanstack/virtual-core@3.13.15 ## 4.0.2 ### Patch Changes - Updated dependencies [[`6d9274c`](https://github.com/TanStack/virtual/commit/6d9274c3f0a9e64450b5829872079a65277bc654)]: - @tanstack/virtual-core@3.13.14 ## 4.0.1 ### Patch Changes - Fix: Notify framework when count changes to update getTotalSize() ([#1085](https://github.com/TanStack/virtual/pull/1085)) Fixed an issue where `getTotalSize()` would return stale values when the `count` option changed (e.g., during filtering or search operations). The virtualizer now automatically notifies the framework when measurement-affecting options change, ensuring the UI updates correctly without requiring manual `useMemo` workarounds. **Before**: When filtering items, the list container would maintain its previous height, causing excessive blank space (when count decreased) or inaccessible items (when count increased). **After**: Height updates automatically when count changes, providing the correct user experience. This fix applies to all framework adapters and has minimal performance impact (< 0.1ms per change). - Updated dependencies [[`2542c5a`](https://github.com/TanStack/virtual/commit/2542c5a3d6820cea956fa3b4f94c42e3526a8d68), [`96e32a6`](https://github.com/TanStack/virtual/commit/96e32a6ffc125743a0172ea4e0fe37ac29c4187b)]: - @tanstack/virtual-core@3.13.13 ## 4.0.0 ### Major Changes - feat(angular-virtual): add support for angular 20 ([#1040](https://github.com/TanStack/virtual/pull/1040)) BREAKING CHANGE: minimum Angular version is now 18.1.0 ## 3.13.12 ### Patch Changes - Updated dependencies [[`d21ed98`](https://github.com/TanStack/virtual/commit/d21ed98da3470b9986c9a028ed70fdf0d6189ab4)]: - @tanstack/virtual-core@3.13.12 ## 3.13.11 ### Patch Changes - Updated dependencies [[`73fa867`](https://github.com/TanStack/virtual/commit/73fa86752599a4bffba51ec8e4ff2f8cb8283010)]: - @tanstack/virtual-core@3.13.11 ## 3.13.10 ### Patch Changes - Updated dependencies [[`b3b7e7d`](https://github.com/TanStack/virtual/commit/b3b7e7dc8b25daeebbd2da61b3b7ae3448babbdb)]: - @tanstack/virtual-core@3.13.10 ## 3.13.9 ### Patch Changes - Updated dependencies [[`9e33cdb`](https://github.com/TanStack/virtual/commit/9e33cdb1c8780c2f455aafc11a0aeea58b71fc69)]: - @tanstack/virtual-core@3.13.9 ## 3.13.8 ### Patch Changes - Updated dependencies [[`60719f6`](https://github.com/TanStack/virtual/commit/60719f61b589d6f9d886e4f7c093217f6d693faf)]: - @tanstack/virtual-core@3.13.8 ## 3.13.7 ### Patch Changes - Updated dependencies [[`e2d93c2`](https://github.com/TanStack/virtual/commit/e2d93c2dcde9ccf60f658e56edccd8d05aefeee6)]: - @tanstack/virtual-core@3.13.7 ## 3.13.6 ### Patch Changes - Updated dependencies [[`042616f`](https://github.com/TanStack/virtual/commit/042616f39ced842470db0b4b40fca77f22454b7f)]: - @tanstack/virtual-core@3.13.6 ## 3.13.5 ### Patch Changes - Updated dependencies [[`51656d9`](https://github.com/TanStack/virtual/commit/51656d94a2469a065e631f25ffc8ec0288d9f5ec)]: - @tanstack/virtual-core@3.13.5 ## 3.13.4 ### Patch Changes - Updated dependencies [[`514b62d`](https://github.com/TanStack/virtual/commit/514b62d04974c2fd59fc8a68ed40f4c1a1547dd2), [`f03d814`](https://github.com/TanStack/virtual/commit/f03d8142c03ea0f5816161a4dad38ca35469841c)]: - @tanstack/virtual-core@3.13.4 ## 3.13.3 ### Patch Changes - Updated dependencies [[`02ef309`](https://github.com/TanStack/virtual/commit/02ef3097de4a14ed4077ace2ca901dc411bf81c1)]: - @tanstack/virtual-core@3.13.3 ================================================ FILE: packages/angular-virtual/README.md ================================================ # Angular Virtual Efficiently virtualize only the visible DOM nodes within massive scrollable elements using Angular, while maintaining complete control over markup and styles. # Quick Start > NOTE: Angular Virtual requires Angular 17. 1. Install `@tanstack/angular-virtual` ```bash $ npm i @tanstack/angular-virtual ``` or ```bash $ pnpm add @tanstack/angular-virtual ``` or ```bash $ yarn add @tanstack/angular-virtual ``` or ```bash $ bun add @tanstack/angular-virtual ``` 2. Inject a virtualizer `@tanstack/angular-virtual` utilizes a helper function `injectVirtualizer` to create the virtualizer and integrate it with the component lifecycle: ```ts import { Component, ElementRef, viewChild } from '@angular/core' import { injectVirtualizer } from '@tanstack/angular-virtual' @Component({ selector: 'my-virtualized-list', template: `
@for (row of virtualizer.getVirtualItems(); track row.index) {
Row {{ row.index }}
}
`, }) export class MyVirtualizedList { scrollElement = viewChild>('scrollElement') virtualizer = injectVirtualizer(() => ({ scrollElement: this.scrollElement(), count: 1000, estimateSize: () => 35, overscan: 5, })) } ``` Note that a [ViewChild](https://angular.dev/api/core/viewChild) is used to get a reference to the scrolling container to allow the virtualizer to interact with it. The adapter will automatically unwrap the [ElementRef](https://angular.dev/api/core/ElementRef) for you. You can also create a virtualizer that attaches to the Window with `injectWindowVirtualizer`: ```ts import { Component } from '@angular/core' import { injectWindowVirtualizer } from '@tanstack/angular-virtual' @Component({ selector: 'my-window-virtualized-list', template: `
@for (row of virtualizer.getVirtualItems(); track row.index) {
Row {{ row.index }}
}
`, }) export class MyWindowVirtualizedList { virtualizer = injectWindowVirtualizer(() => ({ count: 1000, estimateSize: () => 35, overscan: 5, })) } ``` 3. If you need to update options on your virtualizer dynamically, make sure to use signals. ```ts import { Component, input } from '@angular/core' import { injectVirtualizer } from '@tanstack/angular-virtual' @Component({...}) export class MyVirtualizedList { items = input>() virtualizer = injectVirtualizer(() => ({ scrollElement: this.scrollElement(), count: this.items().length, estimateSize: () => 35, overscan: 5, })) } ``` For more examples and detailed usage, visit the [official documentation](https://tanstack.com/virtual/latest). ================================================ FILE: packages/angular-virtual/eslint.config.js ================================================ // @ts-check import rootConfig from '../../eslint.config.js' export default [...rootConfig] ================================================ FILE: packages/angular-virtual/ng-package.json ================================================ { "$schema": "./node_modules/ng-packagr/ng-package.schema.json", "lib": { "entryFile": "src/index.ts" }, "allowedNonPeerDependencies": ["@tanstack/virtual-core"], "dest": "build", "deleteDestPath": false } ================================================ FILE: packages/angular-virtual/package.json ================================================ { "name": "@tanstack/angular-virtual", "version": "4.0.11", "description": "Headless UI for virtualizing scrollable elements in Angular", "author": "Garrett Darnell", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/TanStack/virtual.git", "directory": "packages/angular-virtual" }, "homepage": "https://tanstack.com/virtual", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "keywords": [ "angular", "virtual", "virtual-core" ], "type": "module", "module": "build/fesm2022/tanstack-angular-virtual.mjs", "types": "build/index.d.ts", "exports": { ".": { "types": "./build/index.d.ts", "esm2022": "./build/esm2022/tanstack-angular-virtual.mjs", "esm": "./build/esm2022/tanstack-angular-virtual.mjs", "default": "./build/fesm2022/tanstack-angular-virtual.mjs" }, "./package.json": { "default": "./package.json" } }, "engines": { "node": ">=12" }, "files": [ "build" ], "scripts": { "clean": "premove ./build", "test:types": "tsc --noEmit", "test:eslint": "eslint ./src", "build": "ng-packagr -p ng-package.json -c tsconfig.build.json" }, "dependencies": { "@tanstack/virtual-core": "workspace:*", "tslib": "^2.8.1" }, "devDependencies": { "@angular/core": "^18.1.0", "ng-packagr": "^18.1.0", "typescript": "5.4.5" }, "peerDependencies": { "@angular/core": ">=18.1.0" }, "sideEffects": false } ================================================ FILE: packages/angular-virtual/src/index.ts ================================================ import { DestroyRef, afterNextRender, computed, effect, inject, signal, untracked, } from '@angular/core' import { Virtualizer, elementScroll, observeElementOffset, observeElementRect, observeWindowOffset, observeWindowRect, windowScroll, } from '@tanstack/virtual-core' import { proxyVirtualizer } from './proxy' import type { ElementRef, Signal } from '@angular/core' import type { PartialKeys, VirtualizerOptions } from '@tanstack/virtual-core' import type { AngularVirtualizer } from './types' export * from '@tanstack/virtual-core' export * from './types' function createVirtualizerBase< TScrollElement extends Element | Window, TItemElement extends Element, >( options: Signal>, ): AngularVirtualizer { let virtualizer: Virtualizer function lazyInit() { virtualizer ??= new Virtualizer(options()) return virtualizer } const virtualizerSignal = signal(virtualizer!, { equal: () => false }) // two-way sync options effect( () => { const _options = options() lazyInit() virtualizerSignal.set(virtualizer) virtualizer.setOptions({ ..._options, onChange: (instance, sync) => { // update virtualizerSignal so that dependent computeds recompute. virtualizerSignal.set(instance) _options.onChange?.(instance, sync) }, }) // update virtualizerSignal so that dependent computeds recompute. virtualizerSignal.set(virtualizer) }, { allowSignalWrites: true }, ) const scrollElement = computed(() => options().getScrollElement()) // let the virtualizer know when the scroll element is changed effect( () => { const el = scrollElement() if (el) { untracked(virtualizerSignal)._willUpdate() } }, { allowSignalWrites: true }, ) let cleanup: () => void | undefined afterNextRender({ read: () => (virtualizer ?? lazyInit())._didMount() }) inject(DestroyRef).onDestroy(() => cleanup?.()) return proxyVirtualizer(virtualizerSignal, lazyInit) } export function injectVirtualizer< TScrollElement extends Element, TItemElement extends Element, >( options: () => PartialKeys< Omit, 'getScrollElement'>, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' > & { scrollElement: ElementRef | TScrollElement | undefined }, ): AngularVirtualizer { const resolvedOptions = computed(() => { return { observeElementRect: observeElementRect, observeElementOffset: observeElementOffset, scrollToFn: elementScroll, getScrollElement: () => { const elementOrRef = options().scrollElement return ( (isElementRef(elementOrRef) ? elementOrRef.nativeElement : elementOrRef) ?? null ) }, ...options(), } }) return createVirtualizerBase(resolvedOptions) } function isElementRef( elementOrRef: ElementRef | T | undefined, ): elementOrRef is ElementRef { return elementOrRef != null && 'nativeElement' in elementOrRef } export function injectWindowVirtualizer( options: () => PartialKeys< VirtualizerOptions, | 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): AngularVirtualizer { const resolvedOptions = computed(() => { return { getScrollElement: () => (typeof document !== 'undefined' ? window : null), observeElementRect: observeWindowRect, observeElementOffset: observeWindowOffset, scrollToFn: windowScroll, initialOffset: () => typeof document !== 'undefined' ? window.scrollY : 0, ...options(), } }) return createVirtualizerBase(resolvedOptions) } ================================================ FILE: packages/angular-virtual/src/proxy.ts ================================================ import { computed, untracked } from '@angular/core' import type { Signal, WritableSignal } from '@angular/core' import type { Virtualizer } from '@tanstack/virtual-core' import type { AngularVirtualizer } from './types' export function proxyVirtualizer< V extends Virtualizer, S extends Element | Window = V extends Virtualizer ? U : never, I extends Element = V extends Virtualizer ? U : never, >( virtualizerSignal: WritableSignal, lazyInit: () => V, ): AngularVirtualizer { return new Proxy(virtualizerSignal, { apply() { return virtualizerSignal() }, get(target, property) { const untypedTarget = target as any if (untypedTarget[property]) { return untypedTarget[property] } let virtualizer = untracked(virtualizerSignal) if (virtualizer == null) { virtualizer = lazyInit() untracked(() => virtualizerSignal.set(virtualizer)) } // Create computed signals for each property that represents a reactive value if ( typeof property === 'string' && [ 'getTotalSize', 'getVirtualItems', 'isScrolling', 'options', 'range', 'scrollDirection', 'scrollElement', 'scrollOffset', 'scrollRect', 'measureElementCache', 'measurementsCache', ].includes(property) ) { const isFunction = typeof virtualizer[property as keyof V] === 'function' Object.defineProperty(untypedTarget, property, { value: isFunction ? computed(() => (target()[property as keyof V] as Function)()) : computed(() => target()[property as keyof V]), configurable: true, enumerable: true, }) } // Create plain signals for functions that accept arguments and return reactive values if ( typeof property === 'string' && [ 'getOffsetForAlignment', 'getOffsetForIndex', 'getVirtualItemForOffset', 'indexFromElement', ].includes(property) ) { const fn = virtualizer[property as keyof V] as Function Object.defineProperty(untypedTarget, property, { value: toComputed(virtualizerSignal, fn), configurable: true, enumerable: true, }) } return untypedTarget[property] || virtualizer[property as keyof V] }, has(_, property: string) { return !!untracked(virtualizerSignal)[property as keyof V] }, ownKeys() { return Reflect.ownKeys(untracked(virtualizerSignal)) }, getOwnPropertyDescriptor() { return { enumerable: true, configurable: true, } }, }) as unknown as AngularVirtualizer } function toComputed>( signal: Signal, fn: Function, ) { const computedCache: Record> = {} return (...args: Array) => { // Cache computeds by their arguments to avoid re-creating the computed on each call const serializedArgs = serializeArgs(...args) if (computedCache.hasOwnProperty(serializedArgs)) { return computedCache[serializedArgs]?.() } const computedSignal = computed(() => { void signal() return fn(...args) }) computedCache[serializedArgs] = computedSignal return computedSignal() } } function serializeArgs(...args: Array) { return JSON.stringify(args) } ================================================ FILE: packages/angular-virtual/src/types.ts ================================================ import type { Signal } from '@angular/core' import type { Virtualizer } from '@tanstack/virtual-core' export type AngularVirtualizer< TScrollElement extends Element | Window, TItemElement extends Element, > = Omit< Virtualizer, | 'getTotalSize' | 'getVirtualItems' | 'isScrolling' | 'options' | 'range' | 'scrollDirection' | 'scrollElement' | 'scrollOffset' | 'scrollRect' > & { getTotalSize: Signal< ReturnType['getTotalSize']> > getVirtualItems: Signal< ReturnType['getVirtualItems']> > isScrolling: Signal['isScrolling']> options: Signal['options']> range: Signal['range']> scrollDirection: Signal< Virtualizer['scrollDirection'] > scrollElement: Signal< Virtualizer['scrollElement'] > scrollOffset: Signal< Virtualizer['scrollOffset'] > scrollRect: Signal['scrollRect']> } ================================================ FILE: packages/angular-virtual/tsconfig.build.json ================================================ { "extends": "./node_modules/ng-packagr/lib/ts/conf/tsconfig.ngc.json", "compilerOptions": { "moduleResolution": "bundler", "allowJs": true, "moduleDetection": "force", "module": "ESNext" }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true }, "include": ["src/**/*.ts"] } ================================================ FILE: packages/angular-virtual/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./build/lib" }, "angularCompilerOptions": { "compilationMode": "partial" }, "include": ["src", "eslint.config.js"], "exclude": ["**/*.spec.ts"] } ================================================ FILE: packages/lit-virtual/CHANGELOG.md ================================================ # @tanstack/lit-virtual ## 3.13.24 ### Patch Changes - Updated dependencies [[`7ece2d5`](https://github.com/TanStack/virtual/commit/7ece2d5d4249b7e703c68ac497ae5545c54e7c67)]: - @tanstack/virtual-core@3.13.23 ## 3.13.23 ### Patch Changes - Updated dependencies [[`54d771a`](https://github.com/TanStack/virtual/commit/54d771a7d4c74f6968e8132b5a85f3e04682376a), [`d3416c3`](https://github.com/TanStack/virtual/commit/d3416c386c6446957f413db2eef3211f5fdf3b5f)]: - @tanstack/virtual-core@3.13.22 ## 3.13.22 ### Patch Changes - Updated dependencies [[`be89e29`](https://github.com/TanStack/virtual/commit/be89e293ea01654df6334dc6473b65eebed13e51)]: - @tanstack/virtual-core@3.13.21 ## 3.13.21 ### Patch Changes - Updated dependencies [[`ff83e94`](https://github.com/TanStack/virtual/commit/ff83e949408ba8a714436fa10cafc3725a56274b)]: - @tanstack/virtual-core@3.13.20 ## 3.13.20 ### Patch Changes - Updated dependencies [[`843109c`](https://github.com/TanStack/virtual/commit/843109c5bf780591a762f9767f3808fd15e3f94e)]: - @tanstack/virtual-core@3.13.19 ## 3.13.19 ### Patch Changes - Updated dependencies [[`9067574`](https://github.com/TanStack/virtual/commit/9067574f1a0178d30e27bcac70853bdcbf437fec)]: - @tanstack/virtual-core@3.13.18 ## 3.13.18 ### Patch Changes - Updated dependencies [[`21d9a46`](https://github.com/TanStack/virtual/commit/21d9a46eac034cb4299872891694965bceed526d)]: - @tanstack/virtual-core@3.13.17 ## 3.13.17 ### Patch Changes - Updated dependencies [[`db6df21`](https://github.com/TanStack/virtual/commit/db6df212ed83dd7e4eb6450d1340c95475667b7b)]: - @tanstack/virtual-core@3.13.16 ## 3.13.16 ### Patch Changes - Updated dependencies [[`5a273bf`](https://github.com/TanStack/virtual/commit/5a273bf0c0bc0255ca172929f021c3b6e50cb69d)]: - @tanstack/virtual-core@3.13.15 ## 3.13.15 ### Patch Changes - Updated dependencies [[`6d9274c`](https://github.com/TanStack/virtual/commit/6d9274c3f0a9e64450b5829872079a65277bc654)]: - @tanstack/virtual-core@3.13.14 ## 3.13.14 ### Patch Changes - Fix: Notify framework when count changes to update getTotalSize() ([#1085](https://github.com/TanStack/virtual/pull/1085)) Fixed an issue where `getTotalSize()` would return stale values when the `count` option changed (e.g., during filtering or search operations). The virtualizer now automatically notifies the framework when measurement-affecting options change, ensuring the UI updates correctly without requiring manual `useMemo` workarounds. **Before**: When filtering items, the list container would maintain its previous height, causing excessive blank space (when count decreased) or inaccessible items (when count increased). **After**: Height updates automatically when count changes, providing the correct user experience. This fix applies to all framework adapters and has minimal performance impact (< 0.1ms per change). - Updated dependencies [[`2542c5a`](https://github.com/TanStack/virtual/commit/2542c5a3d6820cea956fa3b4f94c42e3526a8d68), [`96e32a6`](https://github.com/TanStack/virtual/commit/96e32a6ffc125743a0172ea4e0fe37ac29c4187b)]: - @tanstack/virtual-core@3.13.13 ## 3.13.13 ### Patch Changes - fix(lit-virtual): create Virtualizer instance before hostConnected ([#1061](https://github.com/TanStack/virtual/pull/1061)) When creating an instance of the reactive controller in `connectedCallback`, calling `addController` will synchronously call `hostConnected` on the controller. This means that `this.virtualizer` will still be `undefined`. ## 3.13.12 ### Patch Changes - Updated dependencies [[`d21ed98`](https://github.com/TanStack/virtual/commit/d21ed98da3470b9986c9a028ed70fdf0d6189ab4)]: - @tanstack/virtual-core@3.13.12 ## 3.13.11 ### Patch Changes - Updated dependencies [[`73fa867`](https://github.com/TanStack/virtual/commit/73fa86752599a4bffba51ec8e4ff2f8cb8283010)]: - @tanstack/virtual-core@3.13.11 ## 3.13.10 ### Patch Changes - Updated dependencies [[`b3b7e7d`](https://github.com/TanStack/virtual/commit/b3b7e7dc8b25daeebbd2da61b3b7ae3448babbdb)]: - @tanstack/virtual-core@3.13.10 ## 3.13.9 ### Patch Changes - Updated dependencies [[`9e33cdb`](https://github.com/TanStack/virtual/commit/9e33cdb1c8780c2f455aafc11a0aeea58b71fc69)]: - @tanstack/virtual-core@3.13.9 ## 3.13.8 ### Patch Changes - Updated dependencies [[`60719f6`](https://github.com/TanStack/virtual/commit/60719f61b589d6f9d886e4f7c093217f6d693faf)]: - @tanstack/virtual-core@3.13.8 ## 3.13.7 ### Patch Changes - Updated dependencies [[`e2d93c2`](https://github.com/TanStack/virtual/commit/e2d93c2dcde9ccf60f658e56edccd8d05aefeee6)]: - @tanstack/virtual-core@3.13.7 ## 3.13.6 ### Patch Changes - Updated dependencies [[`042616f`](https://github.com/TanStack/virtual/commit/042616f39ced842470db0b4b40fca77f22454b7f)]: - @tanstack/virtual-core@3.13.6 ## 3.13.5 ### Patch Changes - Updated dependencies [[`51656d9`](https://github.com/TanStack/virtual/commit/51656d94a2469a065e631f25ffc8ec0288d9f5ec)]: - @tanstack/virtual-core@3.13.5 ## 3.13.4 ### Patch Changes - Updated dependencies [[`514b62d`](https://github.com/TanStack/virtual/commit/514b62d04974c2fd59fc8a68ed40f4c1a1547dd2), [`f03d814`](https://github.com/TanStack/virtual/commit/f03d8142c03ea0f5816161a4dad38ca35469841c)]: - @tanstack/virtual-core@3.13.4 ## 3.13.3 ### Patch Changes - Updated dependencies [[`02ef309`](https://github.com/TanStack/virtual/commit/02ef3097de4a14ed4077ace2ca901dc411bf81c1)]: - @tanstack/virtual-core@3.13.3 ================================================ FILE: packages/lit-virtual/README.md ================================================ # @tanstack/lit-virtual Efficiently virtualize only the visible DOM nodes within massive scrollable elements using Lit, while maintaining complete control over markup and styles. ## `VirtualizerController` `@tanstack/lit-virtual` utilizes [Reactive Controllers](https://lit.dev/docs/composition/controllers/) to create the virtualizer and integrate it with the element lifecycle: ```ts import { LitElement } from 'lit' import { VirtualizerController } from '@tanstack/lit-virtual' import { Ref, createRef } from 'lit/directives/ref.js' class MyVirtualElement extends LitElement { private virtualizerController: VirtualizerController private scrollElementRef: Ref = createRef() constructor() { super() this.virtualizerController = new VirtualizerController(this, { getScrollElement: () => this.scrollElementRef.value, count: 10000, estimateSize: () => 35, overscan: 5, }) } render() { const virtualizer = this.virtualizerController.getVirtualizer() const virtualItems = virtualizer.getVirtualItems() return html`
${virtualItems.map( (item) => html`
${item.index}
`, )}
` } } ``` Note that a [Ref](https://lit.dev/docs/templates/directives/#ref) is attached to the scrolling container to allow the virtualizer to interact with it. ## `WindowVirtualizerController` You can also create a virtualizer controller that attaches to the Window: ```ts import { WindowVirtualizerController } from '@tanstack/lit-virtual' class MyWindowVirtualElement extends LitElement { private windowVirtualizerController: WindowVirtualizerController constructor() { super() this.windowVirtualizerController = new WindowVirtualizerController(this, { count: this.data.length, estimateSize: () => 350, overscan: 5, }) } // Implement render and other lifecycle methods as needed } ``` For more examples and detailed usage, visit the [official documentation](https://tanstack.com/virtual/latest). ================================================ FILE: packages/lit-virtual/eslint.config.js ================================================ // @ts-check import rootConfig from '../../eslint.config.js' export default [...rootConfig] ================================================ FILE: packages/lit-virtual/package.json ================================================ { "name": "@tanstack/lit-virtual", "version": "3.13.24", "description": "Headless UI for virtualizing scrollable elements in Lit", "author": "Tanner Linsley", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/TanStack/virtual.git", "directory": "packages/lit-virtual" }, "homepage": "https://tanstack.com/virtual", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "keywords": [ "lit", "virtual", "virtual-core", "datagrid" ], "scripts": { "clean": "premove ./dist ./coverage", "test:eslint": "eslint ./src", "test:types": "tsc", "test:build": "publint --strict", "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch", "build": "vite build" }, "type": "module", "types": "dist/esm/index.d.ts", "main": "dist/cjs/index.cjs", "module": "dist/esm/index.js", "exports": { ".": { "import": { "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.js" }, "require": { "types": "./dist/cjs/index.d.cts", "default": "./dist/cjs/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "dist", "src" ], "dependencies": { "@tanstack/virtual-core": "workspace:*" }, "devDependencies": { "@open-wc/testing": "^4.0.0", "lit": "^3.3.0" }, "peerDependencies": { "lit": "^3.1.0" } } ================================================ FILE: packages/lit-virtual/src/index.ts ================================================ import { Virtualizer, elementScroll, observeElementOffset, observeElementRect, observeWindowOffset, observeWindowRect, windowScroll, } from '@tanstack/virtual-core' import type { ReactiveController, ReactiveControllerHost } from 'lit' import type { PartialKeys, VirtualizerOptions } from '@tanstack/virtual-core' class VirtualizerControllerBase< TScrollElement extends Element | Window, TItemElement extends Element, > implements ReactiveController { host: ReactiveControllerHost private readonly virtualizer: Virtualizer private cleanup: () => void = () => {} constructor( host: ReactiveControllerHost, options: VirtualizerOptions, ) { const resolvedOptions: VirtualizerOptions = { ...options, onChange: (instance, sync) => { this.host.updateComplete.then(() => this.host.requestUpdate()) options.onChange?.(instance, sync) }, } this.virtualizer = new Virtualizer(resolvedOptions) ;(this.host = host).addController(this) } public getVirtualizer() { return this.virtualizer } hostConnected() { this.cleanup = this.virtualizer._didMount() } hostUpdated() { this.virtualizer._willUpdate() } hostDisconnected() { this.cleanup() } } export class VirtualizerController< TScrollElement extends Element, TItemElement extends Element, > extends VirtualizerControllerBase { constructor( host: ReactiveControllerHost, options: PartialKeys< VirtualizerOptions, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ) { super(host, { observeElementRect: observeElementRect, observeElementOffset: observeElementOffset, scrollToFn: elementScroll, ...options, }) } } export class WindowVirtualizerController< TItemElement extends Element, > extends VirtualizerControllerBase { constructor( host: ReactiveControllerHost, options: PartialKeys< VirtualizerOptions, | 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ) { super(host, { getScrollElement: () => (typeof document !== 'undefined' ? window : null), observeElementRect: observeWindowRect, observeElementOffset: observeWindowOffset, scrollToFn: windowScroll, initialOffset: () => typeof document !== 'undefined' ? window.scrollY : 0, ...options, }) } } ================================================ FILE: packages/lit-virtual/tests/index.test.ts ================================================ import { describe, test, expect, beforeEach } from 'vitest' import { elementUpdated, fixture, html, waitUntil } from '@open-wc/testing' import { LitElement } from 'lit' import { VirtualizerController } from '../src' import { createRef, ref, Ref } from 'lit/directives/ref.js' import { repeat } from 'lit/directives/repeat.js' import { customElement } from 'lit/decorators.js' const width = 400 const height = 400 const count = 50 @customElement('test-list') class List extends LitElement { private scrollElementRef: Ref = createRef() private virtualizerController: VirtualizerController constructor() { super() this.virtualizerController = new VirtualizerController(this, { getScrollElement: () => this.scrollElementRef.value, count, estimateSize: () => 50, observeElementRect: (_, cb) => { cb({ height, width }) }, }) } render() { const virtualizer = this.virtualizerController.getVirtualizer() const virtualRows = virtualizer.getVirtualItems() return html`
${repeat( virtualRows, (virtualRow) => virtualRow.key, (virtualRow) => html`
Row ${virtualRow.index}
Item ${virtualRow.index}
`, )}
` } } test('should render', async () => { const el = await fixture(html``) await elementUpdated(el) expect(el).toBeTruthy() }) test('should render virtual items', async () => { const el = await fixture(html``) await elementUpdated(el) await waitUntil( () => el.shadowRoot.querySelector('[data-index="4"]'), 'Element did not render virtual items', ) }) ================================================ FILE: packages/lit-virtual/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "experimentalDecorators": true }, "include": ["src", "eslint.config.js", "vite.config.ts"] } ================================================ FILE: packages/lit-virtual/vite.config.ts ================================================ import { defineConfig, mergeConfig } from 'vitest/config' import { tanstackViteConfig } from '@tanstack/vite-config' import packageJson from './package.json' const config = defineConfig({ test: { name: packageJson.name, dir: './tests', watch: false, environment: 'jsdom', }, }) export default mergeConfig( config, tanstackViteConfig({ entry: './src/index.ts', srcDir: './src', }), ) ================================================ FILE: packages/react-virtual/CHANGELOG.md ================================================ # @tanstack/react-virtual ## 3.13.23 ### Patch Changes - Updated dependencies [[`7ece2d5`](https://github.com/TanStack/virtual/commit/7ece2d5d4249b7e703c68ac497ae5545c54e7c67)]: - @tanstack/virtual-core@3.13.23 ## 3.13.22 ### Patch Changes - Updated dependencies [[`54d771a`](https://github.com/TanStack/virtual/commit/54d771a7d4c74f6968e8132b5a85f3e04682376a), [`d3416c3`](https://github.com/TanStack/virtual/commit/d3416c386c6446957f413db2eef3211f5fdf3b5f)]: - @tanstack/virtual-core@3.13.22 ## 3.13.21 ### Patch Changes - Updated dependencies [[`be89e29`](https://github.com/TanStack/virtual/commit/be89e293ea01654df6334dc6473b65eebed13e51)]: - @tanstack/virtual-core@3.13.21 ## 3.13.20 ### Patch Changes - Updated dependencies [[`ff83e94`](https://github.com/TanStack/virtual/commit/ff83e949408ba8a714436fa10cafc3725a56274b)]: - @tanstack/virtual-core@3.13.20 ## 3.13.19 ### Patch Changes - Updated dependencies [[`843109c`](https://github.com/TanStack/virtual/commit/843109c5bf780591a762f9767f3808fd15e3f94e)]: - @tanstack/virtual-core@3.13.19 ## 3.13.18 ### Patch Changes - Updated dependencies [[`9067574`](https://github.com/TanStack/virtual/commit/9067574f1a0178d30e27bcac70853bdcbf437fec)]: - @tanstack/virtual-core@3.13.18 ## 3.13.17 ### Patch Changes - Updated dependencies [[`21d9a46`](https://github.com/TanStack/virtual/commit/21d9a46eac034cb4299872891694965bceed526d)]: - @tanstack/virtual-core@3.13.17 ## 3.13.16 ### Patch Changes - Updated dependencies [[`db6df21`](https://github.com/TanStack/virtual/commit/db6df212ed83dd7e4eb6450d1340c95475667b7b)]: - @tanstack/virtual-core@3.13.16 ## 3.13.15 ### Patch Changes - feat(react-virtual): add `useFlushSync` option ([#1100](https://github.com/TanStack/virtual/pull/1100)) Adds a React-specific `useFlushSync` option to control whether `flushSync` is used for synchronous scroll correction during measurement. The default behavior remains unchanged (`useFlushSync: true`) to preserve the best scrolling experience. Disabling it avoids the React 19 warning about calling `flushSync` during render, at the cost of potentially increased visible whitespace during fast scrolling with dynamically sized items. - Updated dependencies [[`5a273bf`](https://github.com/TanStack/virtual/commit/5a273bf0c0bc0255ca172929f021c3b6e50cb69d)]: - @tanstack/virtual-core@3.13.15 ## 3.13.14 ### Patch Changes - Updated dependencies [[`6d9274c`](https://github.com/TanStack/virtual/commit/6d9274c3f0a9e64450b5829872079a65277bc654)]: - @tanstack/virtual-core@3.13.14 ## 3.13.13 ### Patch Changes - Fix: Notify framework when count changes to update getTotalSize() ([#1085](https://github.com/TanStack/virtual/pull/1085)) Fixed an issue where `getTotalSize()` would return stale values when the `count` option changed (e.g., during filtering or search operations). The virtualizer now automatically notifies the framework when measurement-affecting options change, ensuring the UI updates correctly without requiring manual `useMemo` workarounds. **Before**: When filtering items, the list container would maintain its previous height, causing excessive blank space (when count decreased) or inaccessible items (when count increased). **After**: Height updates automatically when count changes, providing the correct user experience. This fix applies to all framework adapters and has minimal performance impact (< 0.1ms per change). - Updated dependencies [[`2542c5a`](https://github.com/TanStack/virtual/commit/2542c5a3d6820cea956fa3b4f94c42e3526a8d68), [`96e32a6`](https://github.com/TanStack/virtual/commit/96e32a6ffc125743a0172ea4e0fe37ac29c4187b)]: - @tanstack/virtual-core@3.13.13 ## 3.13.12 ### Patch Changes - chore(react-virtual): fix vite e2e build ([#1030](https://github.com/TanStack/virtual/pull/1030)) - Updated dependencies [[`d21ed98`](https://github.com/TanStack/virtual/commit/d21ed98da3470b9986c9a028ed70fdf0d6189ab4)]: - @tanstack/virtual-core@3.13.12 ## 3.13.11 ### Patch Changes - Updated dependencies [[`73fa867`](https://github.com/TanStack/virtual/commit/73fa86752599a4bffba51ec8e4ff2f8cb8283010)]: - @tanstack/virtual-core@3.13.11 ## 3.13.10 ### Patch Changes - Updated dependencies [[`b3b7e7d`](https://github.com/TanStack/virtual/commit/b3b7e7dc8b25daeebbd2da61b3b7ae3448babbdb)]: - @tanstack/virtual-core@3.13.10 ## 3.13.9 ### Patch Changes - Updated dependencies [[`9e33cdb`](https://github.com/TanStack/virtual/commit/9e33cdb1c8780c2f455aafc11a0aeea58b71fc69)]: - @tanstack/virtual-core@3.13.9 ## 3.13.8 ### Patch Changes - Updated dependencies [[`60719f6`](https://github.com/TanStack/virtual/commit/60719f61b589d6f9d886e4f7c093217f6d693faf)]: - @tanstack/virtual-core@3.13.8 ## 3.13.7 ### Patch Changes - Updated dependencies [[`e2d93c2`](https://github.com/TanStack/virtual/commit/e2d93c2dcde9ccf60f658e56edccd8d05aefeee6)]: - @tanstack/virtual-core@3.13.7 ## 3.13.6 ### Patch Changes - Updated dependencies [[`042616f`](https://github.com/TanStack/virtual/commit/042616f39ced842470db0b4b40fca77f22454b7f)]: - @tanstack/virtual-core@3.13.6 ## 3.13.5 ### Patch Changes - Updated dependencies [[`51656d9`](https://github.com/TanStack/virtual/commit/51656d94a2469a065e631f25ffc8ec0288d9f5ec)]: - @tanstack/virtual-core@3.13.5 ## 3.13.4 ### Patch Changes - Updated dependencies [[`514b62d`](https://github.com/TanStack/virtual/commit/514b62d04974c2fd59fc8a68ed40f4c1a1547dd2), [`f03d814`](https://github.com/TanStack/virtual/commit/f03d8142c03ea0f5816161a4dad38ca35469841c)]: - @tanstack/virtual-core@3.13.4 ## 3.13.3 ### Patch Changes - Updated dependencies [[`02ef309`](https://github.com/TanStack/virtual/commit/02ef3097de4a14ed4077ace2ca901dc411bf81c1)]: - @tanstack/virtual-core@3.13.3 ================================================ FILE: packages/react-virtual/e2e/app/measure-element/index.html ================================================
================================================ FILE: packages/react-virtual/e2e/app/measure-element/main.tsx ================================================ import React from 'react' import ReactDOM from 'react-dom/client' import { useVirtualizer } from '@tanstack/react-virtual' interface Item { id: string label: string } const INITIAL_ITEMS: Array = [ { id: 'item-a', label: 'A' }, { id: 'item-b', label: 'B' }, { id: 'item-c', label: 'C' }, ] const App = () => { const parentRef = React.useRef(null) const [items, setItems] = React.useState(INITIAL_ITEMS) const [expandedId, setExpandedId] = React.useState(null) const rowVirtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 36, getItemKey: (index) => items[index].id, }) const toggleExpand = (id: string) => { setExpandedId((prev) => (prev === id ? null : id)) } const deleteItem = (id: string) => { setItems((prev) => prev.filter((item) => item.id !== id)) if (expandedId === id) { setExpandedId(null) } } return (
{rowVirtualizer.getVirtualItems().map((v) => { const item = items[v.index] const isExpanded = expandedId === item.id return (
Row {item.label}
{isExpanded && (
Expanded content for {item.label}
)}
) })}
{rowVirtualizer.getTotalSize()}
) } ReactDOM.createRoot(document.getElementById('root')!).render() ================================================ FILE: packages/react-virtual/e2e/app/scroll/index.html ================================================
================================================ FILE: packages/react-virtual/e2e/app/scroll/main.tsx ================================================ import React from 'react' import ReactDOM from 'react-dom/client' import { useVirtualizer } from '@tanstack/react-virtual' function getRandomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min } const randomHeight = (() => { const cache = new Map() return (id: string) => { const value = cache.get(id) if (value !== undefined) { return value } const v = getRandomInt(25, 100) cache.set(id, v) return v } })() const App = () => { const parentRef = React.useRef(null) const initialOffset = Number( new URLSearchParams(window.location.search).get('initialOffset') ?? 0, ) const rowVirtualizer = useVirtualizer({ count: 1002, getScrollElement: () => parentRef.current, estimateSize: () => 50, initialOffset, debug: true, }) return (
{rowVirtualizer.getVirtualItems().map((v) => (
Row {v.index}
))}
) } ReactDOM.createRoot(document.getElementById('root')!).render() ================================================ FILE: packages/react-virtual/e2e/app/smooth-scroll/index.html ================================================
================================================ FILE: packages/react-virtual/e2e/app/smooth-scroll/main.tsx ================================================ import React from 'react' import ReactDOM from 'react-dom/client' import { useVirtualizer } from '@tanstack/react-virtual' function getRandomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min } const randomHeight = (() => { const cache = new Map() return (id: string) => { const value = cache.get(id) if (value !== undefined) { return value } const v = getRandomInt(25, 100) cache.set(id, v) return v } })() const App = () => { const parentRef = React.useRef(null) const rowVirtualizer = useVirtualizer({ count: 1002, getScrollElement: () => parentRef.current, estimateSize: () => 50, }) return (
{rowVirtualizer.getVirtualItems().map((v) => (
Row {v.index}
))}
) } ReactDOM.createRoot(document.getElementById('root')!).render() ================================================ FILE: packages/react-virtual/e2e/app/stale-index/index.html ================================================
================================================ FILE: packages/react-virtual/e2e/app/stale-index/main.tsx ================================================ import React from 'react' import ReactDOM from 'react-dom/client' import { useVirtualizer } from '@tanstack/react-virtual' /** * Regression test app for stale index bug: * When items are removed from the end of the list, the ResizeObserver may fire * for a disconnected node whose data-index >= the new count. If getItemKey * indexes into the items array, this causes an out-of-bounds error. */ interface Item { id: string label: string } function makeItems(count: number): Array { return Array.from({ length: count }, (_, i) => ({ id: `item-${i}`, label: `Row ${i}`, })) } const App = () => { const parentRef = React.useRef(null) const [items, setItems] = React.useState(() => makeItems(20)) const [error, setError] = React.useState(null) const rowVirtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, getItemKey: (index) => { if (index < 0 || index >= items.length) { const msg = `getItemKey called with stale index ${index} (count=${items.length})` setError(msg) throw new Error(msg) } return items[index].id }, }) const removeLastFive = () => { setItems((prev) => prev.slice(0, Math.max(0, prev.length - 5))) } return (
Count: {items.length}
{error &&
{error}
}
{rowVirtualizer.getVirtualItems().map((v) => { const item = items[v.index] return (
{item.label}
) })}
) } ReactDOM.createRoot(document.getElementById('root')!).render() ================================================ FILE: packages/react-virtual/e2e/app/test/measure-element.spec.ts ================================================ import { expect, test } from '@playwright/test' test('positions items correctly after expand → collapse → delete → expand', async ({ page, }) => { await page.goto('/measure-element/') // All 3 items visible at ~36px each await expect(page.locator('[data-testid="item-a"]')).toBeVisible() await expect(page.locator('[data-testid="item-b"]')).toBeVisible() await expect(page.locator('[data-testid="item-c"]')).toBeVisible() // Step 1: Expand A → should grow to ~160px await page.click('[data-testid="expand-item-a"]') await expect(page.locator('[data-testid="content-item-a"]')).toBeVisible() // Step 2: Collapse A → back to ~36px await page.click('[data-testid="expand-item-a"]') await expect(page.locator('[data-testid="content-item-a"]')).not.toBeVisible() // Step 3: Delete A await page.click('[data-testid="delete-item-a"]') await expect(page.locator('[data-testid="item-a"]')).not.toBeVisible() // Step 4: Expand B → should grow to ~160px await page.click('[data-testid="expand-item-b"]') await expect(page.locator('[data-testid="content-item-b"]')).toBeVisible() // Wait for ResizeObserver to measure the expanded B await page.waitForTimeout(200) // C should be positioned after the expanded B, not overlapping it const bBox = await page.locator('[data-testid="item-b"]').boundingBox() const cBox = await page.locator('[data-testid="item-c"]').boundingBox() expect(bBox).not.toBeNull() expect(cBox).not.toBeNull() // C's top should be at or after B's bottom (with no overlap) const bBottom = bBox!.y + bBox!.height expect(cBox!.y).toBeGreaterThanOrEqual(bBottom - 1) // 1px tolerance }) ================================================ FILE: packages/react-virtual/e2e/app/test/scroll.spec.ts ================================================ import { expect, test } from '@playwright/test' const check = () => { const item = document.querySelector('[data-testid="item-1000"]') const container = document.querySelector('#scroll-container') if (!item || !container) throw new Error('Elements not found') const itemRect = item.getBoundingClientRect() const containerRect = container.getBoundingClientRect() const scrollTop = container.scrollTop const top = itemRect.top + scrollTop - containerRect.top const botttom = top + itemRect.height const containerBottom = scrollTop + container.clientHeight return Math.abs(botttom - containerBottom) } test('scrolls to index 1000', async ({ page }) => { await page.goto('/scroll/') await page.click('#scroll-to-1000') // Wait for scroll effect (including retries) await page.waitForTimeout(1000) await expect(page.locator('[data-testid="item-1000"]')).toBeVisible() const delta = await page.evaluate(check) expect(delta).toBeLessThan(1.01) }) test('scrolls to last item', async ({ page }) => { await page.goto('/scroll/') await page.click('#scroll-to-last') await page.waitForTimeout(1000) // Last item (index 1001) should be visible await expect(page.locator('[data-testid="item-1001"]')).toBeVisible() // Container should be scrolled to the very bottom const atBottom = await page.evaluate(() => { const container = document.querySelector('#scroll-container') if (!container) throw new Error('Container not found') return Math.abs( container.scrollTop + container.clientHeight - container.scrollHeight, ) }) expect(atBottom).toBeLessThan(1.01) }) test('renders correctly with initialOffset and user scroll up', async ({ page, }) => { // Start at offset 5000 (no programmatic scrollToIndex) await page.goto('/scroll/?initialOffset=5000') await page.waitForTimeout(500) // Items around offset 5000 should be visible const visibleIndex = await page.evaluate(() => { const container = document.querySelector('#scroll-container') if (!container) throw new Error('Container not found') const items = container.querySelectorAll('[data-index]') const indices = Array.from(items).map((el) => Number(el.getAttribute('data-index')), ) return Math.min(...indices) }) expect(visibleIndex).toBeGreaterThan(0) // Scroll up by 2000px (user scroll, not programmatic) await page.evaluate(() => { const container = document.querySelector('#scroll-container') if (!container) throw new Error('Container not found') container.scrollTop -= 2000 }) await page.waitForTimeout(500) // After scroll up, items should be properly measured and positioned // (no gaps, no overlaps) — verify consecutive items are contiguous const layout = await page.evaluate(() => { const container = document.querySelector('#scroll-container') if (!container) throw new Error('Container not found') const items = Array.from(container.querySelectorAll('[data-index]')) .map((el) => { const rect = el.getBoundingClientRect() return { index: Number(el.getAttribute('data-index')), top: rect.top, bottom: rect.bottom, height: rect.height, } }) .sort((a, b) => a.index - b.index) // Check that each item's top matches the previous item's bottom (within tolerance) let maxGap = 0 for (let i = 1; i < items.length; i++) { const gap = Math.abs(items[i].top - items[i - 1].bottom) maxGap = Math.max(maxGap, gap) } return { items, maxGap } }) expect(layout.items.length > 0).toBe(true) expect(layout.items.length).toBeGreaterThan(3) // Items should be contiguous — no gaps between consecutive items expect(layout.maxGap).toBeLessThan(2) }) test('scrolls to index 0', async ({ page }) => { await page.goto('/scroll/') // First scroll down await page.click('#scroll-to-1000') await page.waitForTimeout(1000) // Then scroll to first item await page.click('#scroll-to-0') await page.waitForTimeout(1000) await expect(page.locator('[data-testid="item-0"]')).toBeVisible() const scrollTop = await page.evaluate(() => { const container = document.querySelector('#scroll-container') return container?.scrollTop ?? -1 }) expect(scrollTop).toBeLessThan(1.01) }) ================================================ FILE: packages/react-virtual/e2e/app/test/smooth-scroll.spec.ts ================================================ import { expect, test } from '@playwright/test' test('smooth scrolls to index 1000', async ({ page }) => { await page.goto('/smooth-scroll/') await page.click('#scroll-to-1000') // Smooth scroll animation is 500ms + reconciliation time await page.waitForTimeout(2000) await expect(page.locator('[data-testid="item-1000"]')).toBeVisible() const delta = await page.evaluate(() => { const item = document.querySelector('[data-testid="item-1000"]') const container = document.querySelector('#scroll-container') if (!item || !container) throw new Error('Elements not found') const itemRect = item.getBoundingClientRect() const containerRect = container.getBoundingClientRect() const scrollTop = container.scrollTop const top = itemRect.top + scrollTop - containerRect.top const bottom = top + itemRect.height const containerBottom = scrollTop + container.clientHeight return Math.abs(bottom - containerBottom) }) expect(delta).toBeLessThan(1.01) }) test('smooth scrolls to index 100', async ({ page }) => { await page.goto('/smooth-scroll/') await page.click('#scroll-to-100') await page.waitForTimeout(2000) await expect(page.locator('[data-testid="item-100"]')).toBeVisible() }) test('smooth scrolls to index 0 after scrolling away', async ({ page }) => { await page.goto('/smooth-scroll/') // First scroll down await page.click('#scroll-to-500') await page.waitForTimeout(2000) await expect(page.locator('[data-testid="item-500"]')).toBeVisible() // Then smooth scroll back to top await page.click('#scroll-to-0') await page.waitForTimeout(2000) await expect(page.locator('[data-testid="item-0"]')).toBeVisible() const scrollTop = await page.evaluate(() => { const container = document.querySelector('#scroll-container') return container?.scrollTop ?? -1 }) expect(scrollTop).toBeLessThan(1.01) }) test('smooth scrolls to index 500 with start alignment', async ({ page }) => { await page.goto('/smooth-scroll/') await page.click('#scroll-to-500-start') await page.waitForTimeout(2000) await expect(page.locator('[data-testid="item-500"]')).toBeVisible() const delta = await page.evaluate( ([idx, align]) => { const item = document.querySelector(`[data-testid="item-${idx}"]`) const container = document.querySelector('#scroll-container') if (!item || !container) throw new Error('Elements not found') const itemRect = item.getBoundingClientRect() const containerRect = container.getBoundingClientRect() if (align === 'start') { return Math.abs(itemRect.top - containerRect.top) } return 0 }, [500, 'start'] as const, ) expect(delta).toBeLessThan(1.01) }) test('smooth scrolls to index 500 with center alignment', async ({ page }) => { await page.goto('/smooth-scroll/') await page.click('#scroll-to-500-center') await page.waitForTimeout(2000) await expect(page.locator('[data-testid="item-500"]')).toBeVisible() const delta = await page.evaluate( ([idx]) => { const item = document.querySelector(`[data-testid="item-${idx}"]`) const container = document.querySelector('#scroll-container') if (!item || !container) throw new Error('Elements not found') const itemRect = item.getBoundingClientRect() const containerRect = container.getBoundingClientRect() const containerCenter = containerRect.top + containerRect.height / 2 const itemCenter = itemRect.top + itemRect.height / 2 return Math.abs(itemCenter - containerCenter) }, [500] as const, ) // Center alignment has slightly more tolerance due to rounding expect(delta).toBeLessThan(50) }) test('smooth scrolls sequentially to multiple targets', async ({ page }) => { await page.goto('/smooth-scroll/') // Scroll to 100 first await page.click('#scroll-to-100') await page.waitForTimeout(2000) await expect(page.locator('[data-testid="item-100"]')).toBeVisible() // Then scroll to 500 await page.click('#scroll-to-500') await page.waitForTimeout(2000) await expect(page.locator('[data-testid="item-500"]')).toBeVisible() // Then scroll to 1000 await page.click('#scroll-to-1000') await page.waitForTimeout(2000) await expect(page.locator('[data-testid="item-1000"]')).toBeVisible() }) test('interrupting smooth scroll with another smooth scroll', async ({ page, }) => { await page.goto('/smooth-scroll/') // Start scrolling to 1000 await page.click('#scroll-to-1000') // Interrupt mid-animation (before the 500ms animation completes) await page.waitForTimeout(200) await page.click('#scroll-to-100') // Wait for the second scroll to complete await page.waitForTimeout(2000) // Should have ended at 100, not 1000 await expect(page.locator('[data-testid="item-100"]')).toBeVisible() }) ================================================ FILE: packages/react-virtual/e2e/app/test/stale-index.spec.ts ================================================ import { expect, test } from '@playwright/test' test('does not call getItemKey with stale index after removing items', async ({ page, }) => { await page.goto('/stale-index/') // Verify initial state await expect(page.locator('[data-testid="item-count"]')).toHaveText( 'Count: 20', ) // Scroll to the bottom so the last items are rendered and observed by ResizeObserver const container = page.locator('[data-testid="scroll-container"]') await container.evaluate((el) => (el.scrollTop = el.scrollHeight)) await page.waitForTimeout(100) // Remove 5 items from the end — the RO may still fire for the now-disconnected nodes await page.click('[data-testid="remove-items"]') await expect(page.locator('[data-testid="item-count"]')).toHaveText( 'Count: 15', ) // Wait for any pending ResizeObserver callbacks await page.waitForTimeout(200) // No error should have been thrown await expect(page.locator('[data-testid="error"]')).not.toBeVisible() // Remove 5 more to stress it await page.click('[data-testid="remove-items"]') await expect(page.locator('[data-testid="item-count"]')).toHaveText( 'Count: 10', ) await page.waitForTimeout(200) await expect(page.locator('[data-testid="error"]')).not.toBeVisible() }) ================================================ FILE: packages/react-virtual/e2e/app/tsconfig.json ================================================ { "compilerOptions": { "strict": true, "esModuleInterop": true, "jsx": "react-jsx", "target": "ESNext", "moduleResolution": "Bundler", "module": "ESNext", "resolveJsonModule": true, "allowJs": true, "skipLibCheck": true }, "exclude": ["node_modules", "dist"] } ================================================ FILE: packages/react-virtual/e2e/app/vite.config.ts ================================================ import path from 'node:path' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ root: __dirname, plugins: [react()], build: { rollupOptions: { input: { scroll: path.resolve(__dirname, 'scroll/index.html'), 'measure-element': path.resolve( __dirname, 'measure-element/index.html', ), 'smooth-scroll': path.resolve(__dirname, 'smooth-scroll/index.html'), 'stale-index': path.resolve(__dirname, 'stale-index/index.html'), }, }, }, resolve: { alias: { '@tanstack/react-virtual': path.resolve(__dirname, '../../src/index'), '@tanstack/virtual-core': path.resolve( __dirname, '../../../virtual-core/src/index', ), }, }, }) ================================================ FILE: packages/react-virtual/eslint.config.js ================================================ // @ts-check import rootConfig from '../../eslint.config.js' export default [...rootConfig] ================================================ FILE: packages/react-virtual/package.json ================================================ { "name": "@tanstack/react-virtual", "version": "3.13.23", "description": "Headless UI for virtualizing scrollable elements in React", "author": "Tanner Linsley", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/TanStack/virtual.git", "directory": "packages/react-virtual" }, "homepage": "https://tanstack.com/virtual", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "keywords": [ "react", "vue", "solid", "virtual", "virtual-core", "datagrid" ], "scripts": { "clean": "premove ./dist ./coverage", "test:eslint": "eslint ./src", "test:types": "tsc", "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict", "build": "vite build", "test:e2e": "../../node_modules/.bin/playwright test" }, "type": "module", "types": "dist/esm/index.d.ts", "main": "dist/cjs/index.cjs", "module": "dist/esm/index.js", "exports": { ".": { "import": { "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.js" }, "require": { "types": "./dist/cjs/index.d.cts", "default": "./dist/cjs/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "dist", "src" ], "dependencies": { "@tanstack/virtual-core": "workspace:*" }, "devDependencies": { "@testing-library/react": "^16.3.0", "@types/react": "^18.3.23", "@types/react-dom": "^18.3.7", "@vitejs/plugin-react": "^4.5.2", "react": "^18.3.1", "react-dom": "^18.3.1", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } } ================================================ FILE: packages/react-virtual/playwright.config.ts ================================================ import { defineConfig } from '@playwright/test' const PORT = 5173 const baseURL = `http://localhost:${PORT}` export default defineConfig({ testDir: './e2e/app/test', use: { baseURL, }, webServer: { command: `VITE_SERVER_PORT=${PORT} vite build --config e2e/app/vite.config.ts && VITE_SERVER_PORT=${PORT} vite preview --config e2e/app/vite.config.ts --port ${PORT}`, url: `${baseURL}/scroll/`, reuseExistingServer: !process.env.CI, stdout: 'pipe', }, }) ================================================ FILE: packages/react-virtual/src/index.tsx ================================================ import * as React from 'react' import { flushSync } from 'react-dom' import { Virtualizer, elementScroll, observeElementOffset, observeElementRect, observeWindowOffset, observeWindowRect, windowScroll, } from '@tanstack/virtual-core' import type { PartialKeys, VirtualizerOptions } from '@tanstack/virtual-core' export * from '@tanstack/virtual-core' const useIsomorphicLayoutEffect = typeof document !== 'undefined' ? React.useLayoutEffect : React.useEffect export type ReactVirtualizerOptions< TScrollElement extends Element | Window, TItemElement extends Element, > = VirtualizerOptions & { useFlushSync?: boolean } function useVirtualizerBase< TScrollElement extends Element | Window, TItemElement extends Element, >({ useFlushSync = true, ...options }: ReactVirtualizerOptions): Virtualizer< TScrollElement, TItemElement > { const rerender = React.useReducer(() => ({}), {})[1] const resolvedOptions: VirtualizerOptions = { ...options, onChange: (instance, sync) => { if (useFlushSync && sync) { flushSync(rerender) } else { rerender() } options.onChange?.(instance, sync) }, } const [instance] = React.useState( () => new Virtualizer(resolvedOptions), ) instance.setOptions(resolvedOptions) useIsomorphicLayoutEffect(() => { return instance._didMount() }, []) useIsomorphicLayoutEffect(() => { return instance._willUpdate() }) return instance } export function useVirtualizer< TScrollElement extends Element, TItemElement extends Element, >( options: PartialKeys< ReactVirtualizerOptions, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): Virtualizer { return useVirtualizerBase({ observeElementRect: observeElementRect, observeElementOffset: observeElementOffset, scrollToFn: elementScroll, ...options, }) } export function useWindowVirtualizer( options: PartialKeys< ReactVirtualizerOptions, | 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): Virtualizer { return useVirtualizerBase({ getScrollElement: () => (typeof document !== 'undefined' ? window : null), observeElementRect: observeWindowRect, observeElementOffset: observeWindowOffset, scrollToFn: windowScroll, initialOffset: () => (typeof document !== 'undefined' ? window.scrollY : 0), ...options, }) } ================================================ FILE: packages/react-virtual/tests/index.test.tsx ================================================ import { beforeEach, test, expect, vi } from 'vitest' import * as React from 'react' import { render, screen } from '@testing-library/react' import { useVirtualizer, Range } from '../src/index' beforeEach(() => { Object.defineProperties(HTMLElement.prototype, { scrollHeight: { configurable: true, get: () => Number.MAX_SAFE_INTEGER, }, scrollWidth: { configurable: true, get: () => Number.MAX_SAFE_INTEGER, }, }) }) let renderer: vi.Mock interface ListProps { count?: number overscan?: number height?: number width?: number itemSize?: number rangeExtractor?: (range: Range) => number[] dynamic?: boolean } function List({ count = 200, overscan, height = 200, width = 200, itemSize, rangeExtractor, dynamic, }: ListProps) { renderer() const parentRef = React.useRef(null) const elementRectCallbackRef = React.useRef< ((rect: { height: number; width: number }) => void) | null >(null) const rowVirtualizer = useVirtualizer({ count, getScrollElement: () => parentRef.current, estimateSize: () => 50, overscan, observeElementRect: (_, cb) => { cb({ height, width }) elementRectCallbackRef.current = cb }, measureElement: () => itemSize ?? 0, rangeExtractor, }) React.useEffect(() => { elementRectCallbackRef.current?.({ height, width }) }, [height, width]) const measureElement = dynamic ? rowVirtualizer.measureElement : undefined const items = rowVirtualizer.getVirtualItems() return (
{items.map((virtualRow) => (
Row {virtualRow.index}
))}
) } beforeEach(() => { renderer = vi.fn(() => undefined) }) test('should render', () => { render() expect(screen.queryByText('Row 0')).toBeInTheDocument() expect(screen.queryByText('Row 4')).toBeInTheDocument() expect(screen.queryByText('Row 5')).not.toBeInTheDocument() expect(renderer).toHaveBeenCalledTimes(2) }) test('should render with overscan', () => { render() expect(screen.queryByText('Row 0')).toBeInTheDocument() expect(screen.queryByText('Row 3')).toBeInTheDocument() expect(screen.queryByText('Row 4')).not.toBeInTheDocument() expect(renderer).toHaveBeenCalledTimes(2) }) test('should render given dynamic size', async () => { render() expect(screen.queryByText('Row 0')).toBeInTheDocument() expect(screen.queryByText('Row 1')).toBeInTheDocument() expect(screen.queryByText('Row 2')).toBeInTheDocument() expect(screen.queryByText('Row 3')).not.toBeInTheDocument() expect(renderer).toHaveBeenCalledTimes(3) }) test('should use rangeExtractor', () => { render( [0, 1]} />) expect(screen.queryByText('Row 0')).toBeInTheDocument() expect(screen.queryByText('Row 1')).toBeInTheDocument() expect(screen.queryByText('Row 2')).not.toBeInTheDocument() }) test('should handle count change', () => { const { rerender } = render() expect(screen.queryByText('Row 0')).toBeInTheDocument() expect(screen.queryByText('Row 1')).toBeInTheDocument() expect(screen.queryByText('Row 2')).not.toBeInTheDocument() rerender() expect(screen.queryByText('Row 2')).toBeInTheDocument() expect(screen.queryByText('Row 4')).toBeInTheDocument() expect(screen.queryByText('Row 5')).not.toBeInTheDocument() }) test('should handle handle height change', () => { const { rerender } = render() expect(screen.queryByText('Row 0')).not.toBeInTheDocument() rerender() expect(screen.queryByText('Row 0')).toBeInTheDocument() }) ================================================ FILE: packages/react-virtual/tests/test-setup.ts ================================================ import '@testing-library/jest-dom/vitest' import { cleanup } from '@testing-library/react' import { afterEach } from 'vitest' import ResizeObserver from 'resize-observer-polyfill' // https://testing-library.com/docs/react-testing-library/api#cleanup afterEach(() => cleanup()) global.ResizeObserver = ResizeObserver ================================================ FILE: packages/react-virtual/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "jsx": "react" }, "include": [ "src", "eslint.config.js", "vite.config.ts", "playwright.config.ts" ] } ================================================ FILE: packages/react-virtual/vite.config.ts ================================================ import { defineConfig, mergeConfig } from 'vitest/config' import { tanstackViteConfig } from '@tanstack/vite-config' import react from '@vitejs/plugin-react' import packageJson from './package.json' const config = defineConfig({ plugins: [react()], test: { name: packageJson.name, dir: './tests', watch: false, environment: 'jsdom', setupFiles: ['./tests/test-setup.ts'], }, }) export default mergeConfig( config, tanstackViteConfig({ entry: './src/index.tsx', srcDir: './src', }), ) ================================================ FILE: packages/solid-virtual/CHANGELOG.md ================================================ # @tanstack/solid-virtual ## 3.13.23 ### Patch Changes - Updated dependencies [[`7ece2d5`](https://github.com/TanStack/virtual/commit/7ece2d5d4249b7e703c68ac497ae5545c54e7c67)]: - @tanstack/virtual-core@3.13.23 ## 3.13.22 ### Patch Changes - Updated dependencies [[`54d771a`](https://github.com/TanStack/virtual/commit/54d771a7d4c74f6968e8132b5a85f3e04682376a), [`d3416c3`](https://github.com/TanStack/virtual/commit/d3416c386c6446957f413db2eef3211f5fdf3b5f)]: - @tanstack/virtual-core@3.13.22 ## 3.13.21 ### Patch Changes - Updated dependencies [[`be89e29`](https://github.com/TanStack/virtual/commit/be89e293ea01654df6334dc6473b65eebed13e51)]: - @tanstack/virtual-core@3.13.21 ## 3.13.20 ### Patch Changes - Updated dependencies [[`ff83e94`](https://github.com/TanStack/virtual/commit/ff83e949408ba8a714436fa10cafc3725a56274b)]: - @tanstack/virtual-core@3.13.20 ## 3.13.19 ### Patch Changes - Updated dependencies [[`843109c`](https://github.com/TanStack/virtual/commit/843109c5bf780591a762f9767f3808fd15e3f94e)]: - @tanstack/virtual-core@3.13.19 ## 3.13.18 ### Patch Changes - Updated dependencies [[`9067574`](https://github.com/TanStack/virtual/commit/9067574f1a0178d30e27bcac70853bdcbf437fec)]: - @tanstack/virtual-core@3.13.18 ## 3.13.17 ### Patch Changes - Updated dependencies [[`21d9a46`](https://github.com/TanStack/virtual/commit/21d9a46eac034cb4299872891694965bceed526d)]: - @tanstack/virtual-core@3.13.17 ## 3.13.16 ### Patch Changes - Updated dependencies [[`db6df21`](https://github.com/TanStack/virtual/commit/db6df212ed83dd7e4eb6450d1340c95475667b7b)]: - @tanstack/virtual-core@3.13.16 ## 3.13.15 ### Patch Changes - Updated dependencies [[`5a273bf`](https://github.com/TanStack/virtual/commit/5a273bf0c0bc0255ca172929f021c3b6e50cb69d)]: - @tanstack/virtual-core@3.13.15 ## 3.13.14 ### Patch Changes - Updated dependencies [[`6d9274c`](https://github.com/TanStack/virtual/commit/6d9274c3f0a9e64450b5829872079a65277bc654)]: - @tanstack/virtual-core@3.13.14 ## 3.13.13 ### Patch Changes - Fix: Notify framework when count changes to update getTotalSize() ([#1085](https://github.com/TanStack/virtual/pull/1085)) Fixed an issue where `getTotalSize()` would return stale values when the `count` option changed (e.g., during filtering or search operations). The virtualizer now automatically notifies the framework when measurement-affecting options change, ensuring the UI updates correctly without requiring manual `useMemo` workarounds. **Before**: When filtering items, the list container would maintain its previous height, causing excessive blank space (when count decreased) or inaccessible items (when count increased). **After**: Height updates automatically when count changes, providing the correct user experience. This fix applies to all framework adapters and has minimal performance impact (< 0.1ms per change). - Updated dependencies [[`2542c5a`](https://github.com/TanStack/virtual/commit/2542c5a3d6820cea956fa3b4f94c42e3526a8d68), [`96e32a6`](https://github.com/TanStack/virtual/commit/96e32a6ffc125743a0172ea4e0fe37ac29c4187b)]: - @tanstack/virtual-core@3.13.13 ## 3.13.12 ### Patch Changes - Updated dependencies [[`d21ed98`](https://github.com/TanStack/virtual/commit/d21ed98da3470b9986c9a028ed70fdf0d6189ab4)]: - @tanstack/virtual-core@3.13.12 ## 3.13.11 ### Patch Changes - Updated dependencies [[`73fa867`](https://github.com/TanStack/virtual/commit/73fa86752599a4bffba51ec8e4ff2f8cb8283010)]: - @tanstack/virtual-core@3.13.11 ## 3.13.10 ### Patch Changes - Updated dependencies [[`b3b7e7d`](https://github.com/TanStack/virtual/commit/b3b7e7dc8b25daeebbd2da61b3b7ae3448babbdb)]: - @tanstack/virtual-core@3.13.10 ## 3.13.9 ### Patch Changes - Updated dependencies [[`9e33cdb`](https://github.com/TanStack/virtual/commit/9e33cdb1c8780c2f455aafc11a0aeea58b71fc69)]: - @tanstack/virtual-core@3.13.9 ## 3.13.8 ### Patch Changes - Updated dependencies [[`60719f6`](https://github.com/TanStack/virtual/commit/60719f61b589d6f9d886e4f7c093217f6d693faf)]: - @tanstack/virtual-core@3.13.8 ## 3.13.7 ### Patch Changes - Updated dependencies [[`e2d93c2`](https://github.com/TanStack/virtual/commit/e2d93c2dcde9ccf60f658e56edccd8d05aefeee6)]: - @tanstack/virtual-core@3.13.7 ## 3.13.6 ### Patch Changes - Updated dependencies [[`042616f`](https://github.com/TanStack/virtual/commit/042616f39ced842470db0b4b40fca77f22454b7f)]: - @tanstack/virtual-core@3.13.6 ## 3.13.5 ### Patch Changes - Updated dependencies [[`51656d9`](https://github.com/TanStack/virtual/commit/51656d94a2469a065e631f25ffc8ec0288d9f5ec)]: - @tanstack/virtual-core@3.13.5 ## 3.13.4 ### Patch Changes - Updated dependencies [[`514b62d`](https://github.com/TanStack/virtual/commit/514b62d04974c2fd59fc8a68ed40f4c1a1547dd2), [`f03d814`](https://github.com/TanStack/virtual/commit/f03d8142c03ea0f5816161a4dad38ca35469841c)]: - @tanstack/virtual-core@3.13.4 ## 3.13.3 ### Patch Changes - Updated dependencies [[`02ef309`](https://github.com/TanStack/virtual/commit/02ef3097de4a14ed4077ace2ca901dc411bf81c1)]: - @tanstack/virtual-core@3.13.3 ================================================ FILE: packages/solid-virtual/eslint.config.js ================================================ // @ts-check import rootConfig from '../../eslint.config.js' export default [...rootConfig] ================================================ FILE: packages/solid-virtual/package.json ================================================ { "name": "@tanstack/solid-virtual", "version": "3.13.23", "description": "Headless UI for virtualizing scrollable elements in Solid", "author": "Tanner Linsley", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/TanStack/virtual.git", "directory": "packages/solid-virtual" }, "homepage": "https://tanstack.com/virtual", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "keywords": [ "react", "vue", "solid", "virtual", "virtual-core", "datagrid" ], "scripts": { "clean": "premove ./dist ./coverage", "test:eslint": "eslint ./src", "test:types": "tsc", "test:build": "publint --strict", "build": "vite build" }, "type": "module", "types": "dist/esm/index.d.ts", "main": "dist/cjs/index.cjs", "module": "dist/esm/index.js", "exports": { ".": { "import": { "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.js" }, "require": { "types": "./dist/cjs/index.d.cts", "default": "./dist/cjs/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "dist", "src" ], "dependencies": { "@tanstack/virtual-core": "workspace:*" }, "devDependencies": { "solid-js": "^1.9.7", "vite-plugin-solid": "^2.11.6" }, "peerDependencies": { "solid-js": "^1.3.0" } } ================================================ FILE: packages/solid-virtual/src/index.tsx ================================================ import { Virtualizer, elementScroll, observeElementOffset, observeElementRect, observeWindowOffset, observeWindowRect, windowScroll, } from '@tanstack/virtual-core' import { createComputed, createSignal, mergeProps, onCleanup, onMount, } from 'solid-js' import { createStore, reconcile } from 'solid-js/store' import type { PartialKeys, VirtualizerOptions } from '@tanstack/virtual-core' export * from '@tanstack/virtual-core' function createVirtualizerBase< TScrollElement extends Element | Window, TItemElement extends Element, >( options: VirtualizerOptions, ): Virtualizer { const resolvedOptions: VirtualizerOptions = mergeProps(options) const instance = new Virtualizer( resolvedOptions, ) const [virtualItems, setVirtualItems] = createStore( instance.getVirtualItems(), ) const [totalSize, setTotalSize] = createSignal(instance.getTotalSize()) const handler = { get( target: Virtualizer, prop: keyof Virtualizer, ) { switch (prop) { case 'getVirtualItems': return () => virtualItems case 'getTotalSize': return () => totalSize() default: return Reflect.get(target, prop) } }, } const virtualizer = new Proxy(instance, handler) virtualizer.setOptions(resolvedOptions) onMount(() => { const cleanup = virtualizer._didMount() virtualizer._willUpdate() onCleanup(cleanup) }) createComputed(() => { virtualizer.setOptions( mergeProps(resolvedOptions, options, { onChange: ( instance: Virtualizer, sync: boolean, ) => { instance._willUpdate() setVirtualItems( reconcile(instance.getVirtualItems(), { key: 'index', }), ) setTotalSize(instance.getTotalSize()) options.onChange?.(instance, sync) }, }), ) virtualizer.measure() }) return virtualizer } export function createVirtualizer< TScrollElement extends Element, TItemElement extends Element, >( options: PartialKeys< VirtualizerOptions, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): Virtualizer { return createVirtualizerBase( mergeProps( { observeElementRect: observeElementRect, observeElementOffset: observeElementOffset, scrollToFn: elementScroll, }, options, ), ) } export function createWindowVirtualizer( options: PartialKeys< VirtualizerOptions, | 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): Virtualizer { return createVirtualizerBase( mergeProps( { getScrollElement: () => typeof document !== 'undefined' ? window : null, observeElementRect: observeWindowRect, observeElementOffset: observeWindowOffset, scrollToFn: windowScroll, initialOffset: () => typeof document !== 'undefined' ? window.scrollY : 0, }, options, ), ) } ================================================ FILE: packages/solid-virtual/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "jsx": "preserve", "jsxImportSource": "solid-js" }, "include": ["src", "eslint.config.js", "vite.config.ts"] } ================================================ FILE: packages/solid-virtual/vite.config.ts ================================================ import { defineConfig, mergeConfig } from 'vitest/config' import { tanstackViteConfig } from '@tanstack/vite-config' import solid from 'vite-plugin-solid' const config = defineConfig({ plugins: [solid()], }) export default mergeConfig( config, tanstackViteConfig({ entry: './src/index.tsx', srcDir: './src', }), ) ================================================ FILE: packages/svelte-virtual/CHANGELOG.md ================================================ # @tanstack/svelte-virtual ## 3.13.23 ### Patch Changes - Updated dependencies [[`7ece2d5`](https://github.com/TanStack/virtual/commit/7ece2d5d4249b7e703c68ac497ae5545c54e7c67)]: - @tanstack/virtual-core@3.13.23 ## 3.13.22 ### Patch Changes - fix(svelte-virtual): force store update when setOptions is called ([#1143](https://github.com/TanStack/virtual/pull/1143)) - Updated dependencies [[`54d771a`](https://github.com/TanStack/virtual/commit/54d771a7d4c74f6968e8132b5a85f3e04682376a), [`d3416c3`](https://github.com/TanStack/virtual/commit/d3416c386c6446957f413db2eef3211f5fdf3b5f)]: - @tanstack/virtual-core@3.13.22 ## 3.13.21 ### Patch Changes - Updated dependencies [[`be89e29`](https://github.com/TanStack/virtual/commit/be89e293ea01654df6334dc6473b65eebed13e51)]: - @tanstack/virtual-core@3.13.21 ## 3.13.20 ### Patch Changes - Updated dependencies [[`ff83e94`](https://github.com/TanStack/virtual/commit/ff83e949408ba8a714436fa10cafc3725a56274b)]: - @tanstack/virtual-core@3.13.20 ## 3.13.19 ### Patch Changes - Updated dependencies [[`843109c`](https://github.com/TanStack/virtual/commit/843109c5bf780591a762f9767f3808fd15e3f94e)]: - @tanstack/virtual-core@3.13.19 ## 3.13.18 ### Patch Changes - Updated dependencies [[`9067574`](https://github.com/TanStack/virtual/commit/9067574f1a0178d30e27bcac70853bdcbf437fec)]: - @tanstack/virtual-core@3.13.18 ## 3.13.17 ### Patch Changes - Updated dependencies [[`21d9a46`](https://github.com/TanStack/virtual/commit/21d9a46eac034cb4299872891694965bceed526d)]: - @tanstack/virtual-core@3.13.17 ## 3.13.16 ### Patch Changes - Updated dependencies [[`db6df21`](https://github.com/TanStack/virtual/commit/db6df212ed83dd7e4eb6450d1340c95475667b7b)]: - @tanstack/virtual-core@3.13.16 ## 3.13.15 ### Patch Changes - Updated dependencies [[`5a273bf`](https://github.com/TanStack/virtual/commit/5a273bf0c0bc0255ca172929f021c3b6e50cb69d)]: - @tanstack/virtual-core@3.13.15 ## 3.13.14 ### Patch Changes - Updated dependencies [[`6d9274c`](https://github.com/TanStack/virtual/commit/6d9274c3f0a9e64450b5829872079a65277bc654)]: - @tanstack/virtual-core@3.13.14 ## 3.13.13 ### Patch Changes - Fix: Notify framework when count changes to update getTotalSize() ([#1085](https://github.com/TanStack/virtual/pull/1085)) Fixed an issue where `getTotalSize()` would return stale values when the `count` option changed (e.g., during filtering or search operations). The virtualizer now automatically notifies the framework when measurement-affecting options change, ensuring the UI updates correctly without requiring manual `useMemo` workarounds. **Before**: When filtering items, the list container would maintain its previous height, causing excessive blank space (when count decreased) or inaccessible items (when count increased). **After**: Height updates automatically when count changes, providing the correct user experience. This fix applies to all framework adapters and has minimal performance impact (< 0.1ms per change). - Updated dependencies [[`2542c5a`](https://github.com/TanStack/virtual/commit/2542c5a3d6820cea956fa3b4f94c42e3526a8d68), [`96e32a6`](https://github.com/TanStack/virtual/commit/96e32a6ffc125743a0172ea4e0fe37ac29c4187b)]: - @tanstack/virtual-core@3.13.13 ## 3.13.12 ### Patch Changes - Updated dependencies [[`d21ed98`](https://github.com/TanStack/virtual/commit/d21ed98da3470b9986c9a028ed70fdf0d6189ab4)]: - @tanstack/virtual-core@3.13.12 ## 3.13.11 ### Patch Changes - Updated dependencies [[`73fa867`](https://github.com/TanStack/virtual/commit/73fa86752599a4bffba51ec8e4ff2f8cb8283010)]: - @tanstack/virtual-core@3.13.11 ## 3.13.10 ### Patch Changes - Updated dependencies [[`b3b7e7d`](https://github.com/TanStack/virtual/commit/b3b7e7dc8b25daeebbd2da61b3b7ae3448babbdb)]: - @tanstack/virtual-core@3.13.10 ## 3.13.9 ### Patch Changes - Updated dependencies [[`9e33cdb`](https://github.com/TanStack/virtual/commit/9e33cdb1c8780c2f455aafc11a0aeea58b71fc69)]: - @tanstack/virtual-core@3.13.9 ## 3.13.8 ### Patch Changes - Updated dependencies [[`60719f6`](https://github.com/TanStack/virtual/commit/60719f61b589d6f9d886e4f7c093217f6d693faf)]: - @tanstack/virtual-core@3.13.8 ## 3.13.7 ### Patch Changes - Updated dependencies [[`e2d93c2`](https://github.com/TanStack/virtual/commit/e2d93c2dcde9ccf60f658e56edccd8d05aefeee6)]: - @tanstack/virtual-core@3.13.7 ## 3.13.6 ### Patch Changes - Updated dependencies [[`042616f`](https://github.com/TanStack/virtual/commit/042616f39ced842470db0b4b40fca77f22454b7f)]: - @tanstack/virtual-core@3.13.6 ## 3.13.5 ### Patch Changes - Updated dependencies [[`51656d9`](https://github.com/TanStack/virtual/commit/51656d94a2469a065e631f25ffc8ec0288d9f5ec)]: - @tanstack/virtual-core@3.13.5 ## 3.13.4 ### Patch Changes - Updated dependencies [[`514b62d`](https://github.com/TanStack/virtual/commit/514b62d04974c2fd59fc8a68ed40f4c1a1547dd2), [`f03d814`](https://github.com/TanStack/virtual/commit/f03d8142c03ea0f5816161a4dad38ca35469841c)]: - @tanstack/virtual-core@3.13.4 ## 3.13.3 ### Patch Changes - Updated dependencies [[`02ef309`](https://github.com/TanStack/virtual/commit/02ef3097de4a14ed4077ace2ca901dc411bf81c1)]: - @tanstack/virtual-core@3.13.3 ================================================ FILE: packages/svelte-virtual/eslint.config.js ================================================ // @ts-check import rootConfig from '../../eslint.config.js' export default [...rootConfig] ================================================ FILE: packages/svelte-virtual/package.json ================================================ { "name": "@tanstack/svelte-virtual", "version": "3.13.23", "description": "Headless UI for virtualizing scrollable elements in Svelte", "author": "Tanner Linsley", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/TanStack/virtual.git", "directory": "packages/svelte-virtual" }, "homepage": "https://tanstack.com/virtual", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "keywords": [ "react", "vue", "solid", "svelte", "virtual", "virtual-core", "datagrid" ], "scripts": { "clean": "premove ./dist ./coverage", "test:eslint": "eslint ./src", "test:types": "tsc", "test:build": "publint --strict", "build": "svelte-package --input ./src --output ./dist" }, "type": "module", "types": "dist/index.d.ts", "module": "dist/index.js", "svelte": "./dist/index.js", "exports": { ".": { "types": "./dist/index.d.ts", "svelte": "./dist/index.js", "import": "./dist/index.js" }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "dist", "src" ], "dependencies": { "@tanstack/virtual-core": "workspace:*" }, "devDependencies": { "@sveltejs/package": "^2.3.11", "@sveltejs/vite-plugin-svelte": "^3.1.2", "svelte": "^4.2.20" }, "peerDependencies": { "svelte": "^3.48.0 || ^4.0.0 || ^5.0.0" } } ================================================ FILE: packages/svelte-virtual/src/index.ts ================================================ import { Virtualizer, elementScroll, observeElementOffset, observeElementRect, observeWindowOffset, observeWindowRect, windowScroll, } from '@tanstack/virtual-core' import { derived, writable } from 'svelte/store' import type { PartialKeys, VirtualizerOptions } from '@tanstack/virtual-core' import type { Readable, Writable } from 'svelte/store' export * from '@tanstack/virtual-core' export type SvelteVirtualizer< TScrollElement extends Element | Window, TItemElement extends Element, > = Omit, 'setOptions'> & { setOptions: ( options: Partial>, ) => void } function createVirtualizerBase< TScrollElement extends Element | Window, TItemElement extends Element, >( initialOptions: VirtualizerOptions, ): Readable> { const virtualizer = new Virtualizer(initialOptions) const originalSetOptions = virtualizer.setOptions // eslint-disable-next-line prefer-const let virtualizerWritable: Writable> const setOptions = ( options: Partial>, ) => { const resolvedOptions = { ...virtualizer.options, ...options, onChange: options.onChange, } originalSetOptions({ ...resolvedOptions, onChange: ( instance: Virtualizer, sync: boolean, ) => { virtualizerWritable.set(instance) resolvedOptions.onChange?.(instance, sync) }, }) virtualizer._willUpdate() // Force store update in case the range didn't change (e.g. count increased // but scroll position stayed the same). Without this, the store only // updates when onChange fires (on range change), so changes like a new // count that don't shift the visible range would not trigger a re-render. virtualizerWritable.set(virtualizer) } virtualizerWritable = writable(virtualizer, () => { setOptions(initialOptions) return virtualizer._didMount() }) return derived(virtualizerWritable, (instance) => Object.assign(instance, { setOptions }), ) } export function createVirtualizer< TScrollElement extends Element, TItemElement extends Element, >( options: PartialKeys< VirtualizerOptions, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): Readable> { return createVirtualizerBase({ observeElementRect: observeElementRect, observeElementOffset: observeElementOffset, scrollToFn: elementScroll, ...options, }) } export function createWindowVirtualizer( options: PartialKeys< VirtualizerOptions, | 'getScrollElement' | 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' >, ): Readable> { return createVirtualizerBase({ getScrollElement: () => (typeof document !== 'undefined' ? window : null), observeElementRect: observeWindowRect, observeElementOffset: observeWindowOffset, scrollToFn: windowScroll, initialOffset: () => (typeof document !== 'undefined' ? window.scrollY : 0), ...options, }) } ================================================ FILE: packages/svelte-virtual/svelte.config.js ================================================ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' const config = { preprocess: vitePreprocess(), } export default config ================================================ FILE: packages/svelte-virtual/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": [ "src/**/*.js", "src/**/*.ts", "src/**/*.svelte", "eslint.config.js", "svelte.config.js", "vite.config.ts" ] } ================================================ FILE: packages/svelte-virtual/vite.config.ts ================================================ import { svelte } from '@sveltejs/vite-plugin-svelte' import { defineConfig } from 'vitest/config' export default defineConfig({ plugins: [svelte()], }) ================================================ FILE: packages/virtual-core/CHANGELOG.md ================================================ # @tanstack/virtual-core ## 3.13.23 ### Patch Changes - fix(virtual-core): remove incorrect elementsCache cleanup using getItemKey ([#1148](https://github.com/TanStack/virtual/pull/1148)) ## 3.13.22 ### Patch Changes - Add 'instant' to ScrollBehavior type to match the W3C spec ([#1122](https://github.com/TanStack/virtual/pull/1122)) - perf(virtual-core): skip sync DOM reads during normal scrolling ([#1146](https://github.com/TanStack/virtual/pull/1146)) ## 3.13.21 ### Patch Changes - fix(virtual-core): smooth scrolling for dynamic item sizes ([#1108](https://github.com/TanStack/virtual/pull/1108)) ## 3.13.20 ### Patch Changes - fix(virtual-core): early return in \_measureElement for disconnected nodes ([#1135](https://github.com/TanStack/virtual/pull/1135)) ## 3.13.19 ### Patch Changes - Fix crash when component unmounts during `scrollToIndex` by adding a null guard for `targetWindow` inside the `requestAnimationFrame` callback ([#1129](https://github.com/TanStack/virtual/pull/1129)) ## 3.13.18 ### Patch Changes - revert(virtual-core): "notify framework when count changes" 2542c5a ([#1112](https://github.com/TanStack/virtual/pull/1112)) ## 3.13.17 ### Patch Changes - fix(virtual-core): preserve auto alignment for visible items when scrolling ([#1110](https://github.com/TanStack/virtual/pull/1110)) ## 3.13.16 ### Patch Changes - fix(virtual-core): improve scrollToIndex reliability in dynamic mode ([#1106](https://github.com/TanStack/virtual/pull/1106)) - Wait extra frame for ResizeObserver measurements before verifying position - Abort pending scroll operations when new scrollToIndex is called ## 3.13.15 ### Patch Changes - fix(virtual-core): scroll to last index properly ([#1105](https://github.com/TanStack/virtual/pull/1105)) ## 3.13.14 ### Patch Changes - Fix: Correct lane assignments when lane count changes dynamically ([#1095](https://github.com/TanStack/virtual/pull/1095)) Fixed a critical bug where changing the number of lanes dynamically would cause layout breakage with incorrect lane assignments. When the lane count changed (e.g., from 3 to 2 columns in a responsive masonry layout), some virtual items would retain their old lane numbers, causing out-of-bounds errors and broken layouts. **Root Cause**: After clearing measurements cache on lane change, the virtualizer was incorrectly restoring data from `initialMeasurementsCache`, which contained stale lane assignments from the previous lane count. **Fix**: Skip `initialMeasurementsCache` restoration during lane transitions by checking the `lanesSettling` flag. This ensures all measurements are recalculated with correct lane assignments for the new lane count. **Before**: ```typescript // With lanes = 2 virtualItems.forEach((item) => { columns[item.lane].push(item) // ❌ Error: item.lane could be 3 }) ``` **After**: ```typescript // With lanes = 2 virtualItems.forEach((item) => { columns[item.lane].push(item) // ✅ item.lane is always 0 or 1 }) ``` This fix is essential for responsive masonry layouts where column count changes based on viewport width. No performance impact as it only affects the lane change transition path. ## 3.13.13 ### Patch Changes - Fix: Notify framework when count changes to update getTotalSize() ([#1085](https://github.com/TanStack/virtual/pull/1085)) Fixed an issue where `getTotalSize()` would return stale values when the `count` option changed (e.g., during filtering or search operations). The virtualizer now automatically notifies the framework when measurement-affecting options change, ensuring the UI updates correctly without requiring manual `useMemo` workarounds. **Before**: When filtering items, the list container would maintain its previous height, causing excessive blank space (when count decreased) or inaccessible items (when count increased). **After**: Height updates automatically when count changes, providing the correct user experience. This fix applies to all framework adapters and has minimal performance impact (< 0.1ms per change). - fix: stabilize lane assignments in masonry layout ([#1080](https://github.com/TanStack/virtual/pull/1080)) Added lane assignment caching to prevent items from jumping between lanes when viewport is resized. Previously, items could shift to different lanes during resize due to recalculating "shortest lane" with slightly different heights. Changes: - Added `laneAssignments` cache (Map) to persist lane assignments - Lane cache is cleared when `lanes` option changes or `measure()` is called - Lane cache is cleaned up when `count` decreases (removes stale entries) - Lane cache is cleared when virtualizer is disabled ## 3.13.12 ### Patch Changes - fix(virtual-core): scroll to index doesn't scroll to bottom correctly ([#1029](https://github.com/TanStack/virtual/pull/1029)) ## 3.13.11 ### Patch Changes - Revert "Adapt default logic to adjust scroll position only on backward scrolling (#1002)" ([#1026](https://github.com/TanStack/virtual/pull/1026)) ## 3.13.10 ### Patch Changes - fix(virtual-core): Adapt default logic to adjust scroll position only on backward scrolling ([#1002](https://github.com/TanStack/virtual/pull/1002)) ## 3.13.9 ### Patch Changes - fix(virtual-core): fix `Error: Unexpected undefined` ([#1004](https://github.com/TanStack/virtual/pull/1004)) ## 3.13.8 ### Patch Changes - fix(virtual-core): loosen approxEqual to allow 1px difference ([#995](https://github.com/TanStack/virtual/pull/995)) ## 3.13.7 ### Patch Changes - fix(virtual-core): prevent measurement jitter when scale is applied ([#986](https://github.com/TanStack/virtual/pull/986)) ## 3.13.6 ### Patch Changes - fix(virtual-core): fix total size calculation for single item in multi-lane ([`042616f`](https://github.com/TanStack/virtual/commit/042616f39ced842470db0b4b40fca77f22454b7f)) ## 3.13.5 ### Patch Changes - fix(core): handle case when item count is less than or equal to lanes ([#964](https://github.com/TanStack/virtual/pull/964)) ## 3.13.4 ### Patch Changes - fix(virtual-core): update maybeNotify cache when deps change ([#957](https://github.com/TanStack/virtual/pull/957)) - fix(virtual-core): set `useScrollendEvent` default to false for bette… ([#951](https://github.com/TanStack/virtual/pull/951)) ## 3.13.3 ### Patch Changes - fix(virtual-core): expand range in masonry layouts to catch items from all lanes ([#937](https://github.com/TanStack/virtual/pull/937)) ================================================ FILE: packages/virtual-core/eslint.config.js ================================================ // @ts-check import rootConfig from '../../eslint.config.js' export default [...rootConfig] ================================================ FILE: packages/virtual-core/package.json ================================================ { "name": "@tanstack/virtual-core", "version": "3.13.23", "description": "Headless UI for virtualizing scrollable elements in TS/JS + Frameworks", "author": "Tanner Linsley", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/TanStack/virtual.git", "directory": "packages/virtual-core" }, "homepage": "https://tanstack.com/virtual", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "keywords": [ "react", "vue", "solid", "virtual", "virtual-core", "datagrid" ], "scripts": { "clean": "premove ./dist ./coverage", "test:eslint": "eslint ./src", "test:types": "tsc", "test:lib": "vitest", "test:lib:dev": "pnpm run test:lib --watch", "test:build": "publint --strict", "build": "vite build" }, "type": "module", "types": "dist/esm/index.d.ts", "main": "dist/cjs/index.cjs", "module": "dist/esm/index.js", "exports": { ".": { "import": { "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.js" }, "require": { "types": "./dist/cjs/index.d.cts", "default": "./dist/cjs/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "dist", "src" ] } ================================================ FILE: packages/virtual-core/src/index.ts ================================================ import { approxEqual, debounce, memo, notUndefined } from './utils' export * from './utils' // type ScrollDirection = 'forward' | 'backward' type ScrollAlignment = 'start' | 'center' | 'end' | 'auto' type ScrollBehavior = 'auto' | 'smooth' | 'instant' export interface ScrollToOptions { align?: ScrollAlignment behavior?: ScrollBehavior } type ScrollToOffsetOptions = ScrollToOptions type ScrollToIndexOptions = ScrollToOptions export interface Range { startIndex: number endIndex: number overscan: number count: number } type Key = number | string | bigint export interface VirtualItem { key: Key index: number start: number end: number size: number lane: number } export interface Rect { width: number height: number } // const getRect = (element: HTMLElement): Rect => { const { offsetWidth, offsetHeight } = element return { width: offsetWidth, height: offsetHeight } } export const defaultKeyExtractor = (index: number) => index export const defaultRangeExtractor = (range: Range) => { const start = Math.max(range.startIndex - range.overscan, 0) const end = Math.min(range.endIndex + range.overscan, range.count - 1) const arr = [] for (let i = start; i <= end; i++) { arr.push(i) } return arr } export const observeElementRect = ( instance: Virtualizer, cb: (rect: Rect) => void, ) => { const element = instance.scrollElement if (!element) { return } const targetWindow = instance.targetWindow if (!targetWindow) { return } const handler = (rect: Rect) => { const { width, height } = rect cb({ width: Math.round(width), height: Math.round(height) }) } handler(getRect(element as unknown as HTMLElement)) if (!targetWindow.ResizeObserver) { return () => {} } const observer = new targetWindow.ResizeObserver((entries) => { const run = () => { const entry = entries[0] if (entry?.borderBoxSize) { const box = entry.borderBoxSize[0] if (box) { handler({ width: box.inlineSize, height: box.blockSize }) return } } handler(getRect(element as unknown as HTMLElement)) } instance.options.useAnimationFrameWithResizeObserver ? requestAnimationFrame(run) : run() }) observer.observe(element, { box: 'border-box' }) return () => { observer.unobserve(element) } } const addEventListenerOptions = { passive: true, } export const observeWindowRect = ( instance: Virtualizer, cb: (rect: Rect) => void, ) => { const element = instance.scrollElement if (!element) { return } const handler = () => { cb({ width: element.innerWidth, height: element.innerHeight }) } handler() element.addEventListener('resize', handler, addEventListenerOptions) return () => { element.removeEventListener('resize', handler) } } const supportsScrollend = typeof window == 'undefined' ? true : 'onscrollend' in window type ObserveOffsetCallBack = (offset: number, isScrolling: boolean) => void export const observeElementOffset = ( instance: Virtualizer, cb: ObserveOffsetCallBack, ) => { const element = instance.scrollElement if (!element) { return } const targetWindow = instance.targetWindow if (!targetWindow) { return } let offset = 0 const fallback = instance.options.useScrollendEvent && supportsScrollend ? () => undefined : debounce( targetWindow, () => { cb(offset, false) }, instance.options.isScrollingResetDelay, ) const createHandler = (isScrolling: boolean) => () => { const { horizontal, isRtl } = instance.options offset = horizontal ? element['scrollLeft'] * ((isRtl && -1) || 1) : element['scrollTop'] fallback() cb(offset, isScrolling) } const handler = createHandler(true) const endHandler = createHandler(false) element.addEventListener('scroll', handler, addEventListenerOptions) const registerScrollendEvent = instance.options.useScrollendEvent && supportsScrollend if (registerScrollendEvent) { element.addEventListener('scrollend', endHandler, addEventListenerOptions) } return () => { element.removeEventListener('scroll', handler) if (registerScrollendEvent) { element.removeEventListener('scrollend', endHandler) } } } export const observeWindowOffset = ( instance: Virtualizer, cb: ObserveOffsetCallBack, ) => { const element = instance.scrollElement if (!element) { return } const targetWindow = instance.targetWindow if (!targetWindow) { return } let offset = 0 const fallback = instance.options.useScrollendEvent && supportsScrollend ? () => undefined : debounce( targetWindow, () => { cb(offset, false) }, instance.options.isScrollingResetDelay, ) const createHandler = (isScrolling: boolean) => () => { offset = element[instance.options.horizontal ? 'scrollX' : 'scrollY'] fallback() cb(offset, isScrolling) } const handler = createHandler(true) const endHandler = createHandler(false) element.addEventListener('scroll', handler, addEventListenerOptions) const registerScrollendEvent = instance.options.useScrollendEvent && supportsScrollend if (registerScrollendEvent) { element.addEventListener('scrollend', endHandler, addEventListenerOptions) } return () => { element.removeEventListener('scroll', handler) if (registerScrollendEvent) { element.removeEventListener('scrollend', endHandler) } } } export const measureElement = ( element: TItemElement, entry: ResizeObserverEntry | undefined, instance: Virtualizer, ) => { if (entry?.borderBoxSize) { const box = entry.borderBoxSize[0] if (box) { const size = Math.round( box[instance.options.horizontal ? 'inlineSize' : 'blockSize'], ) return size } } return (element as unknown as HTMLElement)[ instance.options.horizontal ? 'offsetWidth' : 'offsetHeight' ] } export const windowScroll = ( offset: number, { adjustments = 0, behavior, }: { adjustments?: number; behavior?: ScrollBehavior }, instance: Virtualizer, ) => { const toOffset = offset + adjustments instance.scrollElement?.scrollTo?.({ [instance.options.horizontal ? 'left' : 'top']: toOffset, behavior, }) } export const elementScroll = ( offset: number, { adjustments = 0, behavior, }: { adjustments?: number; behavior?: ScrollBehavior }, instance: Virtualizer, ) => { const toOffset = offset + adjustments instance.scrollElement?.scrollTo?.({ [instance.options.horizontal ? 'left' : 'top']: toOffset, behavior, }) } export interface VirtualizerOptions< TScrollElement extends Element | Window, TItemElement extends Element, > { // Required from the user count: number getScrollElement: () => TScrollElement | null estimateSize: (index: number) => number // Required from the framework adapter (but can be overridden) scrollToFn: ( offset: number, options: { adjustments?: number; behavior?: ScrollBehavior }, instance: Virtualizer, ) => void observeElementRect: ( instance: Virtualizer, cb: (rect: Rect) => void, ) => void | (() => void) observeElementOffset: ( instance: Virtualizer, cb: ObserveOffsetCallBack, ) => void | (() => void) // Optional debug?: boolean initialRect?: Rect onChange?: ( instance: Virtualizer, sync: boolean, ) => void measureElement?: ( element: TItemElement, entry: ResizeObserverEntry | undefined, instance: Virtualizer, ) => number overscan?: number horizontal?: boolean paddingStart?: number paddingEnd?: number scrollPaddingStart?: number scrollPaddingEnd?: number initialOffset?: number | (() => number) getItemKey?: (index: number) => Key rangeExtractor?: (range: Range) => Array scrollMargin?: number gap?: number indexAttribute?: string initialMeasurementsCache?: Array lanes?: number isScrollingResetDelay?: number useScrollendEvent?: boolean enabled?: boolean isRtl?: boolean useAnimationFrameWithResizeObserver?: boolean } type ScrollState = { // what we want index: number | null align: ScrollAlignment behavior: ScrollBehavior // lifecycle startedAt: number // target tracking lastTargetOffset: number // settling stableFrames: number } export class Virtualizer< TScrollElement extends Element | Window, TItemElement extends Element, > { private unsubs: Array void)> = [] options!: Required> scrollElement: TScrollElement | null = null targetWindow: (Window & typeof globalThis) | null = null isScrolling = false private scrollState: ScrollState | null = null measurementsCache: Array = [] private itemSizeCache = new Map() private laneAssignments = new Map() // index → lane cache private pendingMeasuredCacheIndexes: Array = [] private prevLanes: number | undefined = undefined private lanesChangedFlag = false private lanesSettling = false scrollRect: Rect | null = null scrollOffset: number | null = null scrollDirection: ScrollDirection | null = null private scrollAdjustments = 0 shouldAdjustScrollPositionOnItemSizeChange: | undefined | (( item: VirtualItem, delta: number, instance: Virtualizer, ) => boolean) elementsCache = new Map() private now = () => this.targetWindow?.performance?.now?.() ?? Date.now() private observer = (() => { let _ro: ResizeObserver | null = null const get = () => { if (_ro) { return _ro } if (!this.targetWindow || !this.targetWindow.ResizeObserver) { return null } return (_ro = new this.targetWindow.ResizeObserver((entries) => { entries.forEach((entry) => { const run = () => { const node = entry.target as TItemElement const index = this.indexFromElement(node) if (!node.isConnected) { this.observer.unobserve(node) return } if (this.shouldMeasureDuringScroll(index)) { this.resizeItem( index, this.options.measureElement(node, entry, this), ) } } this.options.useAnimationFrameWithResizeObserver ? requestAnimationFrame(run) : run() }) })) } return { disconnect: () => { get()?.disconnect() _ro = null }, observe: (target: Element) => get()?.observe(target, { box: 'border-box' }), unobserve: (target: Element) => get()?.unobserve(target), } })() range: { startIndex: number; endIndex: number } | null = null constructor(opts: VirtualizerOptions) { this.setOptions(opts) } setOptions = (opts: VirtualizerOptions) => { Object.entries(opts).forEach(([key, value]) => { if (typeof value === 'undefined') delete (opts as any)[key] }) this.options = { debug: false, initialOffset: 0, overscan: 1, paddingStart: 0, paddingEnd: 0, scrollPaddingStart: 0, scrollPaddingEnd: 0, horizontal: false, getItemKey: defaultKeyExtractor, rangeExtractor: defaultRangeExtractor, onChange: () => {}, measureElement, initialRect: { width: 0, height: 0 }, scrollMargin: 0, gap: 0, indexAttribute: 'data-index', initialMeasurementsCache: [], lanes: 1, isScrollingResetDelay: 150, enabled: true, isRtl: false, useScrollendEvent: false, useAnimationFrameWithResizeObserver: false, ...opts, } } private notify = (sync: boolean) => { this.options.onChange?.(this, sync) } private maybeNotify = memo( () => { this.calculateRange() return [ this.isScrolling, this.range ? this.range.startIndex : null, this.range ? this.range.endIndex : null, ] }, (isScrolling) => { this.notify(isScrolling) }, { key: process.env.NODE_ENV !== 'production' && 'maybeNotify', debug: () => this.options.debug, initialDeps: [ this.isScrolling, this.range ? this.range.startIndex : null, this.range ? this.range.endIndex : null, ] as [boolean, number | null, number | null], }, ) private cleanup = () => { this.unsubs.filter(Boolean).forEach((d) => d!()) this.unsubs = [] this.observer.disconnect() if (this.rafId != null && this.targetWindow) { this.targetWindow.cancelAnimationFrame(this.rafId) this.rafId = null } this.scrollState = null this.scrollElement = null this.targetWindow = null } _didMount = () => { return () => { this.cleanup() } } _willUpdate = () => { const scrollElement = this.options.enabled ? this.options.getScrollElement() : null if (this.scrollElement !== scrollElement) { this.cleanup() if (!scrollElement) { this.maybeNotify() return } this.scrollElement = scrollElement if (this.scrollElement && 'ownerDocument' in this.scrollElement) { this.targetWindow = this.scrollElement.ownerDocument.defaultView } else { this.targetWindow = this.scrollElement?.window ?? null } this.elementsCache.forEach((cached) => { this.observer.observe(cached) }) this.unsubs.push( this.options.observeElementRect(this, (rect) => { this.scrollRect = rect this.maybeNotify() }), ) this.unsubs.push( this.options.observeElementOffset(this, (offset, isScrolling) => { this.scrollAdjustments = 0 this.scrollDirection = isScrolling ? this.getScrollOffset() < offset ? 'forward' : 'backward' : null this.scrollOffset = offset this.isScrolling = isScrolling if (this.scrollState) { this.scheduleScrollReconcile() } this.maybeNotify() }), ) this._scrollToOffset(this.getScrollOffset(), { adjustments: undefined, behavior: undefined, }) } } private rafId: number | null = null private scheduleScrollReconcile() { if (!this.targetWindow) { this.scrollState = null return } if (this.rafId != null) return this.rafId = this.targetWindow.requestAnimationFrame(() => { this.rafId = null this.reconcileScroll() }) } private reconcileScroll() { if (!this.scrollState) return const el = this.scrollElement if (!el) return // Safety valve: bail out if reconciliation has been running too long const MAX_RECONCILE_MS = 5000 if (this.now() - this.scrollState.startedAt > MAX_RECONCILE_MS) { this.scrollState = null return } const offsetInfo = this.scrollState.index != null ? this.getOffsetForIndex(this.scrollState.index, this.scrollState.align) : undefined const targetOffset = offsetInfo ? offsetInfo[0] : this.scrollState.lastTargetOffset // Require one stable frame where target matches scroll offset. // approxEqual() already tolerates minor fluctuations, so one frame is sufficient // to confirm scroll has reached its target without premature cleanup. const STABLE_FRAMES = 1 const targetChanged = targetOffset !== this.scrollState.lastTargetOffset if (!targetChanged && approxEqual(targetOffset, this.getScrollOffset())) { this.scrollState.stableFrames++ if (this.scrollState.stableFrames >= STABLE_FRAMES) { this.scrollState = null return } } else { this.scrollState.stableFrames = 0 if (targetChanged) { this.scrollState.lastTargetOffset = targetOffset // Switch to 'auto' behavior once measurements cause target to change // We want to jump directly to the correct position, not smoothly animate to it this.scrollState.behavior = 'auto' this._scrollToOffset(targetOffset, { adjustments: undefined, behavior: 'auto', }) } } // Always reschedule while scrollState is active to guarantee // the safety valve timeout runs even if no scroll events fire // (e.g. no-op scrollToFn, detached element) this.scheduleScrollReconcile() } private getSize = () => { if (!this.options.enabled) { this.scrollRect = null return 0 } this.scrollRect = this.scrollRect ?? this.options.initialRect return this.scrollRect[this.options.horizontal ? 'width' : 'height'] } private getScrollOffset = () => { if (!this.options.enabled) { this.scrollOffset = null return 0 } this.scrollOffset = this.scrollOffset ?? (typeof this.options.initialOffset === 'function' ? this.options.initialOffset() : this.options.initialOffset) return this.scrollOffset } private getFurthestMeasurement = ( measurements: Array, index: number, ) => { const furthestMeasurementsFound = new Map() const furthestMeasurements = new Map() for (let m = index - 1; m >= 0; m--) { const measurement = measurements[m]! if (furthestMeasurementsFound.has(measurement.lane)) { continue } const previousFurthestMeasurement = furthestMeasurements.get( measurement.lane, ) if ( previousFurthestMeasurement == null || measurement.end > previousFurthestMeasurement.end ) { furthestMeasurements.set(measurement.lane, measurement) } else if (measurement.end < previousFurthestMeasurement.end) { furthestMeasurementsFound.set(measurement.lane, true) } if (furthestMeasurementsFound.size === this.options.lanes) { break } } return furthestMeasurements.size === this.options.lanes ? Array.from(furthestMeasurements.values()).sort((a, b) => { if (a.end === b.end) { return a.index - b.index } return a.end - b.end })[0] : undefined } private getMeasurementOptions = memo( () => [ this.options.count, this.options.paddingStart, this.options.scrollMargin, this.options.getItemKey, this.options.enabled, this.options.lanes, ], (count, paddingStart, scrollMargin, getItemKey, enabled, lanes) => { const lanesChanged = this.prevLanes !== undefined && this.prevLanes !== lanes if (lanesChanged) { // Set flag for getMeasurements to handle this.lanesChangedFlag = true } this.prevLanes = lanes this.pendingMeasuredCacheIndexes = [] return { count, paddingStart, scrollMargin, getItemKey, enabled, lanes, } }, { key: false, }, ) private getMeasurements = memo( () => [this.getMeasurementOptions(), this.itemSizeCache], ( { count, paddingStart, scrollMargin, getItemKey, enabled, lanes }, itemSizeCache, ) => { if (!enabled) { this.measurementsCache = [] this.itemSizeCache.clear() this.laneAssignments.clear() return [] } // Clean up stale lane cache entries when count decreases if (this.laneAssignments.size > count) { for (const index of this.laneAssignments.keys()) { if (index >= count) { this.laneAssignments.delete(index) } } } // ✅ Force complete recalculation when lanes change if (this.lanesChangedFlag) { this.lanesChangedFlag = false // Reset immediately this.lanesSettling = true // Start settling period this.measurementsCache = [] this.itemSizeCache.clear() this.laneAssignments.clear() // Clear lane cache for new lane count // Clear pending indexes to force min = 0 this.pendingMeasuredCacheIndexes = [] } // Don't restore from initialMeasurementsCache during lane changes // as it contains stale lane assignments from the previous lane count if (this.measurementsCache.length === 0 && !this.lanesSettling) { this.measurementsCache = this.options.initialMeasurementsCache this.measurementsCache.forEach((item) => { this.itemSizeCache.set(item.key, item.size) }) } // ✅ During lanes settling, ignore pendingMeasuredCacheIndexes to prevent repositioning const min = this.lanesSettling ? 0 : this.pendingMeasuredCacheIndexes.length > 0 ? Math.min(...this.pendingMeasuredCacheIndexes) : 0 this.pendingMeasuredCacheIndexes = [] // ✅ End settling period when cache is fully built if (this.lanesSettling && this.measurementsCache.length === count) { this.lanesSettling = false } const measurements = this.measurementsCache.slice(0, min) // ✅ Performance: Track last item index per lane for O(1) lookup const laneLastIndex: Array = new Array(lanes).fill( undefined, ) // Initialize from existing measurements (before min) for (let m = 0; m < min; m++) { const item = measurements[m] if (item) { laneLastIndex[item.lane] = m } } for (let i = min; i < count; i++) { const key = getItemKey(i) // Check for cached lane assignment const cachedLane = this.laneAssignments.get(i) let lane: number let start: number if (cachedLane !== undefined && this.options.lanes > 1) { // Use cached lane - O(1) lookup for previous item in same lane lane = cachedLane const prevIndex = laneLastIndex[lane] const prevInLane = prevIndex !== undefined ? measurements[prevIndex] : undefined start = prevInLane ? prevInLane.end + this.options.gap : paddingStart + scrollMargin } else { // No cache - use original logic (find shortest lane) const furthestMeasurement = this.options.lanes === 1 ? measurements[i - 1] : this.getFurthestMeasurement(measurements, i) start = furthestMeasurement ? furthestMeasurement.end + this.options.gap : paddingStart + scrollMargin lane = furthestMeasurement ? furthestMeasurement.lane : i % this.options.lanes // Cache the lane assignment if (this.options.lanes > 1) { this.laneAssignments.set(i, lane) } } const measuredSize = itemSizeCache.get(key) const size = typeof measuredSize === 'number' ? measuredSize : this.options.estimateSize(i) const end = start + size measurements[i] = { index: i, start, size, end, key, lane, } // ✅ Performance: Update lane's last item index laneLastIndex[lane] = i } this.measurementsCache = measurements return measurements }, { key: process.env.NODE_ENV !== 'production' && 'getMeasurements', debug: () => this.options.debug, }, ) calculateRange = memo( () => [ this.getMeasurements(), this.getSize(), this.getScrollOffset(), this.options.lanes, ], (measurements, outerSize, scrollOffset, lanes) => { return (this.range = measurements.length > 0 && outerSize > 0 ? calculateRange({ measurements, outerSize, scrollOffset, lanes, }) : null) }, { key: process.env.NODE_ENV !== 'production' && 'calculateRange', debug: () => this.options.debug, }, ) getVirtualIndexes = memo( () => { let startIndex: number | null = null let endIndex: number | null = null const range = this.calculateRange() if (range) { startIndex = range.startIndex endIndex = range.endIndex } this.maybeNotify.updateDeps([this.isScrolling, startIndex, endIndex]) return [ this.options.rangeExtractor, this.options.overscan, this.options.count, startIndex, endIndex, ] }, (rangeExtractor, overscan, count, startIndex, endIndex) => { return startIndex === null || endIndex === null ? [] : rangeExtractor({ startIndex, endIndex, overscan, count, }) }, { key: process.env.NODE_ENV !== 'production' && 'getVirtualIndexes', debug: () => this.options.debug, }, ) indexFromElement = (node: TItemElement) => { const attributeName = this.options.indexAttribute const indexStr = node.getAttribute(attributeName) if (!indexStr) { console.warn( `Missing attribute name '${attributeName}={index}' on measured element.`, ) return -1 } return parseInt(indexStr, 10) } /** * Determines if an item at the given index should be measured during smooth scroll. * During smooth scroll, only items within a buffer range around the target are measured * to prevent items far from the target from pushing it away. */ private shouldMeasureDuringScroll = (index: number): boolean => { // No scroll state or not smooth scroll - always allow measurements if (!this.scrollState || this.scrollState.behavior !== 'smooth') { return true } const scrollIndex = this.scrollState.index ?? this.getVirtualItemForOffset(this.scrollState.lastTargetOffset)?.index if (scrollIndex !== undefined && this.range) { // Allow measurements within a buffer range around the scroll target const bufferSize = Math.max( this.options.overscan, Math.ceil((this.range.endIndex - this.range.startIndex) / 2), ) const minIndex = Math.max(0, scrollIndex - bufferSize) const maxIndex = Math.min( this.options.count - 1, scrollIndex + bufferSize, ) return index >= minIndex && index <= maxIndex } return true } measureElement = (node: TItemElement | null) => { if (!node) { this.elementsCache.forEach((cached, key) => { if (!cached.isConnected) { this.observer.unobserve(cached) this.elementsCache.delete(key) } }) return } const index = this.indexFromElement(node) const key = this.options.getItemKey(index) const prevNode = this.elementsCache.get(key) if (prevNode !== node) { if (prevNode) { this.observer.unobserve(prevNode) } this.observer.observe(node) this.elementsCache.set(key, node) } // Sync-measure when idle (initial render) or during programmatic scrolling // (scrollToIndex/scrollToOffset) where reconcileScroll needs sizes in the same frame. // During normal user scrolling, skip sync measurement — the RO callback handles it async. if ( (!this.isScrolling || this.scrollState) && this.shouldMeasureDuringScroll(index) ) { this.resizeItem(index, this.options.measureElement(node, undefined, this)) } } resizeItem = (index: number, size: number) => { const item = this.measurementsCache[index] if (!item) return const itemSize = this.itemSizeCache.get(item.key) ?? item.size const delta = size - itemSize if (delta !== 0) { if ( this.scrollState?.behavior !== 'smooth' && (this.shouldAdjustScrollPositionOnItemSizeChange !== undefined ? this.shouldAdjustScrollPositionOnItemSizeChange(item, delta, this) : item.start < this.getScrollOffset() + this.scrollAdjustments) ) { if (process.env.NODE_ENV !== 'production' && this.options.debug) { console.info('correction', delta) } this._scrollToOffset(this.getScrollOffset(), { adjustments: (this.scrollAdjustments += delta), behavior: undefined, }) } this.pendingMeasuredCacheIndexes.push(item.index) this.itemSizeCache = new Map(this.itemSizeCache.set(item.key, size)) this.notify(false) } } getVirtualItems = memo( () => [this.getVirtualIndexes(), this.getMeasurements()], (indexes, measurements) => { const virtualItems: Array = [] for (let k = 0, len = indexes.length; k < len; k++) { const i = indexes[k]! const measurement = measurements[i]! virtualItems.push(measurement) } return virtualItems }, { key: process.env.NODE_ENV !== 'production' && 'getVirtualItems', debug: () => this.options.debug, }, ) getVirtualItemForOffset = (offset: number) => { const measurements = this.getMeasurements() if (measurements.length === 0) { return undefined } return notUndefined( measurements[ findNearestBinarySearch( 0, measurements.length - 1, (index: number) => notUndefined(measurements[index]).start, offset, ) ], ) } private getMaxScrollOffset = () => { if (!this.scrollElement) return 0 if ('scrollHeight' in this.scrollElement) { // Element return this.options.horizontal ? this.scrollElement.scrollWidth - this.scrollElement.clientWidth : this.scrollElement.scrollHeight - this.scrollElement.clientHeight } else { // Window const doc = this.scrollElement.document.documentElement return this.options.horizontal ? doc.scrollWidth - this.scrollElement.innerWidth : doc.scrollHeight - this.scrollElement.innerHeight } } getOffsetForAlignment = ( toOffset: number, align: ScrollAlignment, itemSize = 0, ) => { if (!this.scrollElement) return 0 const size = this.getSize() const scrollOffset = this.getScrollOffset() if (align === 'auto') { align = toOffset >= scrollOffset + size ? 'end' : 'start' } if (align === 'center') { // When aligning to a particular item (e.g. with scrollToIndex), // adjust offset by the size of the item to center on the item toOffset += (itemSize - size) / 2 } else if (align === 'end') { toOffset -= size } const maxOffset = this.getMaxScrollOffset() return Math.max(Math.min(maxOffset, toOffset), 0) } getOffsetForIndex = (index: number, align: ScrollAlignment = 'auto') => { index = Math.max(0, Math.min(index, this.options.count - 1)) const size = this.getSize() const scrollOffset = this.getScrollOffset() const item = this.measurementsCache[index] if (!item) return if (align === 'auto') { if (item.end >= scrollOffset + size - this.options.scrollPaddingEnd) { align = 'end' } else if (item.start <= scrollOffset + this.options.scrollPaddingStart) { align = 'start' } else { return [scrollOffset, align] as const } } // For the last item with 'end' alignment, use browser's actual max scroll // to account for borders/padding that aren't in our measurements if (align === 'end' && index === this.options.count - 1) { return [this.getMaxScrollOffset(), align] as const } const toOffset = align === 'end' ? item.end + this.options.scrollPaddingEnd : item.start - this.options.scrollPaddingStart return [ this.getOffsetForAlignment(toOffset, align, item.size), align, ] as const } scrollToOffset = ( toOffset: number, { align = 'start', behavior = 'auto' }: ScrollToOffsetOptions = {}, ) => { const offset = this.getOffsetForAlignment(toOffset, align) const now = this.now() this.scrollState = { index: null, align, behavior, startedAt: now, lastTargetOffset: offset, stableFrames: 0, } this._scrollToOffset(offset, { adjustments: undefined, behavior }) this.scheduleScrollReconcile() } scrollToIndex = ( index: number, { align: initialAlign = 'auto', behavior = 'auto', }: ScrollToIndexOptions = {}, ) => { index = Math.max(0, Math.min(index, this.options.count - 1)) const offsetInfo = this.getOffsetForIndex(index, initialAlign) if (!offsetInfo) { return } const [offset, align] = offsetInfo const now = this.now() this.scrollState = { index, align, behavior, startedAt: now, lastTargetOffset: offset, stableFrames: 0, } this._scrollToOffset(offset, { adjustments: undefined, behavior }) this.scheduleScrollReconcile() } scrollBy = ( delta: number, { behavior = 'auto' }: ScrollToOffsetOptions = {}, ) => { const offset = this.getScrollOffset() + delta const now = this.now() this.scrollState = { index: null, align: 'start', behavior, startedAt: now, lastTargetOffset: offset, stableFrames: 0, } this._scrollToOffset(offset, { adjustments: undefined, behavior }) this.scheduleScrollReconcile() } getTotalSize = () => { const measurements = this.getMeasurements() let end: number // If there are no measurements, set the end to paddingStart // If there is only one lane, use the last measurement's end // Otherwise find the maximum end value among all measurements if (measurements.length === 0) { end = this.options.paddingStart } else if (this.options.lanes === 1) { end = measurements[measurements.length - 1]?.end ?? 0 } else { const endByLane = Array(this.options.lanes).fill(null) let endIndex = measurements.length - 1 while (endIndex >= 0 && endByLane.some((val) => val === null)) { const item = measurements[endIndex]! if (endByLane[item.lane] === null) { endByLane[item.lane] = item.end } endIndex-- } end = Math.max(...endByLane.filter((val): val is number => val !== null)) } return Math.max( end - this.options.scrollMargin + this.options.paddingEnd, 0, ) } private _scrollToOffset = ( offset: number, { adjustments, behavior, }: { adjustments: number | undefined behavior: ScrollBehavior | undefined }, ) => { this.options.scrollToFn(offset, { behavior, adjustments }, this) } measure = () => { this.itemSizeCache = new Map() this.laneAssignments = new Map() // Clear lane cache for full re-layout this.notify(false) } } const findNearestBinarySearch = ( low: number, high: number, getCurrentValue: (i: number) => number, value: number, ) => { while (low <= high) { const middle = ((low + high) / 2) | 0 const currentValue = getCurrentValue(middle) if (currentValue < value) { low = middle + 1 } else if (currentValue > value) { high = middle - 1 } else { return middle } } if (low > 0) { return low - 1 } else { return 0 } } function calculateRange({ measurements, outerSize, scrollOffset, lanes, }: { measurements: Array outerSize: number scrollOffset: number lanes: number }) { const lastIndex = measurements.length - 1 const getOffset = (index: number) => measurements[index]!.start // handle case when item count is less than or equal to lanes if (measurements.length <= lanes) { return { startIndex: 0, endIndex: lastIndex, } } let startIndex = findNearestBinarySearch( 0, lastIndex, getOffset, scrollOffset, ) let endIndex = startIndex if (lanes === 1) { while ( endIndex < lastIndex && measurements[endIndex]!.end < scrollOffset + outerSize ) { endIndex++ } } else if (lanes > 1) { // Expand forward until we include the visible items from all lanes // which are closer to the end of the virtualizer window const endPerLane = Array(lanes).fill(0) while ( endIndex < lastIndex && endPerLane.some((pos) => pos < scrollOffset + outerSize) ) { const item = measurements[endIndex]! endPerLane[item.lane] = item.end endIndex++ } // Expand backward until we include all lanes' visible items // closer to the top const startPerLane = Array(lanes).fill(scrollOffset + outerSize) while (startIndex >= 0 && startPerLane.some((pos) => pos >= scrollOffset)) { const item = measurements[startIndex]! startPerLane[item.lane] = item.start startIndex-- } // Align startIndex to the beginning of its lane startIndex = Math.max(0, startIndex - (startIndex % lanes)) // Align endIndex to the end of its lane endIndex = Math.min(lastIndex, endIndex + (lanes - 1 - (endIndex % lanes))) } return { startIndex, endIndex } } ================================================ FILE: packages/virtual-core/src/utils.ts ================================================ export type NoInfer = [A][A extends any ? 0 : never] export type PartialKeys = Omit & Partial> export function memo, TResult>( getDeps: () => [...TDeps], fn: (...args: NoInfer<[...TDeps]>) => TResult, opts: { key: false | string debug?: () => boolean onChange?: (result: TResult) => void initialDeps?: TDeps skipInitialOnChange?: boolean }, ) { let deps = opts.initialDeps ?? [] let result: TResult | undefined let isInitial = true function memoizedFunction(): TResult { let depTime: number if (opts.key && opts.debug?.()) depTime = Date.now() const newDeps = getDeps() const depsChanged = newDeps.length !== deps.length || newDeps.some((dep: any, index: number) => deps[index] !== dep) if (!depsChanged) { return result! } deps = newDeps let resultTime: number if (opts.key && opts.debug?.()) resultTime = Date.now() result = fn(...newDeps) if (opts.key && opts.debug?.()) { const depEndTime = Math.round((Date.now() - depTime!) * 100) / 100 const resultEndTime = Math.round((Date.now() - resultTime!) * 100) / 100 const resultFpsPercentage = resultEndTime / 16 const pad = (str: number | string, num: number) => { str = String(str) while (str.length < num) { str = ' ' + str } return str } console.info( `%c⏱ ${pad(resultEndTime, 5)} /${pad(depEndTime, 5)} ms`, ` font-size: .6rem; font-weight: bold; color: hsl(${Math.max( 0, Math.min(120 - 120 * resultFpsPercentage, 120), )}deg 100% 31%);`, opts?.key, ) } if (opts?.onChange && !(isInitial && opts.skipInitialOnChange)) { opts.onChange(result) } isInitial = false return result } // Attach updateDeps to the function itself memoizedFunction.updateDeps = (newDeps: [...TDeps]) => { deps = newDeps } return memoizedFunction } export function notUndefined(value: T | undefined, msg?: string): T { if (value === undefined) { throw new Error(`Unexpected undefined${msg ? `: ${msg}` : ''}`) } else { return value } } export const approxEqual = (a: number, b: number) => Math.abs(a - b) < 1.01 export const debounce = ( targetWindow: Window & typeof globalThis, fn: Function, ms: number, ) => { let timeoutId: number return function (this: any, ...args: Array) { targetWindow.clearTimeout(timeoutId) timeoutId = targetWindow.setTimeout(() => fn.apply(this, args), ms) } } ================================================ FILE: packages/virtual-core/tests/index.test.ts ================================================ import { expect, test, vi } from 'vitest' import { Virtualizer } from '../src/index' test('should export the Virtualizer class', () => { expect(Virtualizer).toBeDefined() }) test('should return empty items for empty scroll element', () => { const virtualizer = new Virtualizer({ count: 100, getScrollElement: () => null, estimateSize: () => 50, scrollToFn: vi.fn(), observeElementRect: vi.fn(), observeElementOffset: vi.fn(), }) expect(virtualizer.getVirtualItems()).toEqual([]) }) test('should return correct total size with one item and multiple lanes', () => { const virtualizer = new Virtualizer({ count: 1, lanes: 2, estimateSize: () => 50, getScrollElement: () => null, scrollToFn: vi.fn(), observeElementRect: vi.fn(), observeElementOffset: vi.fn(), }) expect(virtualizer.getTotalSize()).toBe(50) }) test('should correctly recalculate lane assignments when lane count changes', () => { // Create a mock scroll element const mockScrollElement = { scrollTop: 0, scrollLeft: 0, offsetWidth: 400, offsetHeight: 600, } as unknown as HTMLDivElement // Mock ResizeObserver let resizeCallback: ((entries: any[]) => void) | null = null const mockResizeObserver = vi.fn((callback) => { resizeCallback = callback return { observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn(), } }) global.ResizeObserver = mockResizeObserver as any // Create virtualizer with 3 lanes initially const virtualizer = new Virtualizer({ count: 10, lanes: 3, estimateSize: () => 100, getScrollElement: () => mockScrollElement, scrollToFn: vi.fn(), observeElementRect: (instance, cb) => { cb({ width: 400, height: 600 }) return () => {} }, observeElementOffset: (instance, cb) => { cb(0, false) return () => {} }, }) virtualizer._willUpdate() // Get initial measurements with 3 lanes let measurements = virtualizer['getMeasurements']() expect(measurements.length).toBe(10) // All lane assignments should be 0, 1, or 2 (3 lanes) measurements.forEach((item) => { expect(item.lane).toBeGreaterThanOrEqual(0) expect(item.lane).toBeLessThan(3) }) // Change to 2 lanes virtualizer.setOptions({ count: 10, lanes: 2, estimateSize: () => 100, getScrollElement: () => mockScrollElement, scrollToFn: vi.fn(), observeElementRect: (instance, cb) => { cb({ width: 400, height: 600 }) return () => {} }, observeElementOffset: (instance, cb) => { cb(0, false) return () => {} }, }) virtualizer._willUpdate() // Get new measurements with 2 lanes measurements = virtualizer['getMeasurements']() expect(measurements.length).toBe(10) // All lane assignments should now be 0 or 1 (2 lanes) // This is the bug fix - previously some items could still have lane: 2 measurements.forEach((item, index) => { expect(item.lane).toBeGreaterThanOrEqual(0) expect(item.lane).toBeLessThan(2) }) // Verify no out of bounds access would occur const lanes = 2 const columns = Array.from({ length: lanes }, () => [] as typeof measurements) measurements.forEach((item) => { // This should not throw expect(() => { columns[item.lane].push(item) }).not.toThrow() }) }) test('should update getTotalSize() when count option changes (filtering/search)', () => { const virtualizer = new Virtualizer({ count: 100, estimateSize: () => 50, getScrollElement: () => null, scrollToFn: vi.fn(), observeElementRect: vi.fn(), observeElementOffset: vi.fn(), }) expect(virtualizer.getTotalSize()).toBe(5000) // 100 × 50 // Simulate filtering - reduce count to 20 virtualizer.setOptions({ count: 20, estimateSize: () => 50, getScrollElement: () => null, scrollToFn: vi.fn(), observeElementRect: vi.fn(), observeElementOffset: vi.fn(), }) // getTotalSize() should immediately return updated value (not stale) expect(virtualizer.getTotalSize()).toBe(1000) // 20 × 50 // Restore full count virtualizer.setOptions({ count: 100, estimateSize: () => 50, getScrollElement: () => null, scrollToFn: vi.fn(), observeElementRect: vi.fn(), observeElementOffset: vi.fn(), }) expect(virtualizer.getTotalSize()).toBe(5000) // 100 × 50 }) test('should not throw when component unmounts during scrollToIndex rAF loop', () => { // Collect rAF callbacks so we can flush them manually const rafCallbacks: Array = [] const mockRaf = vi.fn((cb: FrameRequestCallback) => { rafCallbacks.push(cb) return rafCallbacks.length }) const mockWindow = { requestAnimationFrame: mockRaf, cancelAnimationFrame: vi.fn(), ResizeObserver: vi.fn(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn(), })), } const mockScrollElement = { scrollTop: 0, scrollLeft: 0, scrollWidth: 1000, scrollHeight: 5000, offsetWidth: 400, offsetHeight: 600, ownerDocument: { defaultView: mockWindow, }, } as unknown as HTMLDivElement const virtualizer = new Virtualizer({ count: 100, estimateSize: () => 50, measureElement: (el) => el.getBoundingClientRect().height, getScrollElement: () => mockScrollElement, scrollToFn: vi.fn(), observeElementRect: (instance, cb) => { cb({ width: 400, height: 600 }) return () => {} }, observeElementOffset: (instance, cb) => { cb(0, false) return () => {} }, }) // Initialize the virtualizer so targetWindow is set virtualizer._willUpdate() // Populate elementsCache so isDynamicMode() returns true. // This triggers the code path where the rAF callback calls // this.targetWindow!.requestAnimationFrame(verify) const mockElement = { getBoundingClientRect: () => ({ height: 50 }), isConnected: true, setAttribute: vi.fn(), } as unknown as HTMLElement virtualizer.elementsCache.set(0, mockElement) // Trigger scrollToIndex which schedules a rAF callback virtualizer.scrollToIndex(50) // Simulate component unmount — cleanup sets targetWindow to null const unmount = virtualizer._didMount() unmount() // Flush all pending rAF callbacks — this should not throw // Without the fix, this crashes with: // "Cannot read properties of null (reading 'requestAnimationFrame')" expect(() => { rafCallbacks.forEach((cb) => cb(0)) }).not.toThrow() }) function createMockEnvironment() { const rafCallbacks: Array = [] let rafIdCounter = 0 const mockRaf = vi.fn((cb: FrameRequestCallback) => { rafCallbacks.push(cb) return ++rafIdCounter }) const mockCancelRaf = vi.fn() const mockWindow = { requestAnimationFrame: mockRaf, cancelAnimationFrame: mockCancelRaf, performance: { now: () => Date.now() }, ResizeObserver: vi.fn(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn(), })), } const mockScrollElement = { scrollTop: 0, scrollLeft: 0, scrollWidth: 1000, scrollHeight: 5000, clientWidth: 400, clientHeight: 600, offsetWidth: 400, offsetHeight: 600, ownerDocument: { defaultView: mockWindow, }, scrollTo: vi.fn(), } as unknown as HTMLDivElement const scrollToFn = vi.fn() return { rafCallbacks, mockWindow, mockScrollElement, scrollToFn } } function createVirtualizer( mockScrollElement: HTMLDivElement, scrollToFn: ReturnType, ) { return new Virtualizer({ count: 100, estimateSize: () => 50, getScrollElement: () => mockScrollElement, scrollToFn, observeElementRect: (_instance, cb) => { cb({ width: 400, height: 600 }) return () => {} }, observeElementOffset: (_instance, cb) => { cb(0, false) return () => {} }, }) } test('scrollToIndex(0) should reconcile correctly', () => { const { rafCallbacks, mockScrollElement, scrollToFn } = createMockEnvironment() const virtualizer = createVirtualizer(mockScrollElement, scrollToFn) virtualizer._willUpdate() scrollToFn.mockClear() virtualizer.scrollToIndex(0) // scrollToFn should have been called with offset for index 0 expect(scrollToFn).toHaveBeenCalled() const calledOffset = scrollToFn.mock.calls[0]![0] expect(calledOffset).toBe(0) // Flush rAF — reconcileScroll should run and not bail // It should eventually clear scrollState (settle) rafCallbacks.forEach((cb) => cb(0)) // scrollState should be cleared after settling expect(virtualizer['scrollState']).toBeNull() }) test('scrollToOffset should reconcile and clear scrollState', () => { const { rafCallbacks, mockScrollElement, scrollToFn } = createMockEnvironment() const virtualizer = createVirtualizer(mockScrollElement, scrollToFn) virtualizer._willUpdate() scrollToFn.mockClear() virtualizer.scrollToOffset(200) expect(scrollToFn).toHaveBeenCalled() // scrollState should be set with index: null expect(virtualizer['scrollState']).not.toBeNull() expect(virtualizer['scrollState']!.index).toBeNull() // Simulate the scroll offset reaching the target virtualizer.scrollOffset = 200 // Flush rAF — reconciliation should settle and clear scrollState rafCallbacks.forEach((cb) => cb(0)) expect(virtualizer['scrollState']).toBeNull() }) test('scrollBy should reconcile and clear scrollState', () => { const { rafCallbacks, mockScrollElement, scrollToFn } = createMockEnvironment() const virtualizer = createVirtualizer(mockScrollElement, scrollToFn) virtualizer._willUpdate() scrollToFn.mockClear() virtualizer.scrollBy(100) expect(virtualizer['scrollState']).not.toBeNull() expect(virtualizer['scrollState']!.index).toBeNull() // Simulate scroll offset reaching the target virtualizer.scrollOffset = 100 rafCallbacks.forEach((cb) => cb(0)) expect(virtualizer['scrollState']).toBeNull() }) test('reconcileScroll should bail out after timeout', () => { const { rafCallbacks, mockWindow, mockScrollElement, scrollToFn } = createMockEnvironment() // Make performance.now() return a controllable value let fakeTime = 1000 mockWindow.performance.now = () => fakeTime const virtualizer = createVirtualizer(mockScrollElement, scrollToFn) virtualizer._willUpdate() virtualizer.scrollToIndex(50) expect(virtualizer['scrollState']).not.toBeNull() // Advance time past the 5s safety valve fakeTime = 7000 // Flush rAF — should trigger timeout bailout rafCallbacks.forEach((cb) => cb(0)) expect(virtualizer['scrollState']).toBeNull() }) test('cleanup should cancel pending RAF and clear scrollState', () => { const { mockWindow, mockScrollElement, scrollToFn } = createMockEnvironment() const virtualizer = createVirtualizer(mockScrollElement, scrollToFn) virtualizer._willUpdate() virtualizer.scrollToIndex(50) expect(virtualizer['scrollState']).not.toBeNull() expect(virtualizer['rafId']).not.toBeNull() const unmount = virtualizer._didMount() unmount() expect(virtualizer['scrollState']).toBeNull() expect(virtualizer['rafId']).toBeNull() expect(mockWindow.cancelAnimationFrame).toHaveBeenCalled() }) ================================================ FILE: packages/virtual-core/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src", "eslint.config.js", "vite.config.ts"] } ================================================ FILE: packages/virtual-core/vite.config.ts ================================================ import { defineConfig, mergeConfig } from 'vitest/config' import { tanstackViteConfig } from '@tanstack/vite-config' import packageJson from './package.json' const config = defineConfig({ test: { name: packageJson.name, dir: './tests', watch: false, environment: 'jsdom', }, }) export default mergeConfig( config, tanstackViteConfig({ entry: './src/index.ts', srcDir: './src', }), ) ================================================ FILE: packages/vue-virtual/CHANGELOG.md ================================================ # @tanstack/vue-virtual ## 3.13.23 ### Patch Changes - Updated dependencies [[`7ece2d5`](https://github.com/TanStack/virtual/commit/7ece2d5d4249b7e703c68ac497ae5545c54e7c67)]: - @tanstack/virtual-core@3.13.23 ## 3.13.22 ### Patch Changes - Updated dependencies [[`54d771a`](https://github.com/TanStack/virtual/commit/54d771a7d4c74f6968e8132b5a85f3e04682376a), [`d3416c3`](https://github.com/TanStack/virtual/commit/d3416c386c6446957f413db2eef3211f5fdf3b5f)]: - @tanstack/virtual-core@3.13.22 ## 3.13.21 ### Patch Changes - Updated dependencies [[`be89e29`](https://github.com/TanStack/virtual/commit/be89e293ea01654df6334dc6473b65eebed13e51)]: - @tanstack/virtual-core@3.13.21 ## 3.13.20 ### Patch Changes - Updated dependencies [[`ff83e94`](https://github.com/TanStack/virtual/commit/ff83e949408ba8a714436fa10cafc3725a56274b)]: - @tanstack/virtual-core@3.13.20 ## 3.13.19 ### Patch Changes - Updated dependencies [[`843109c`](https://github.com/TanStack/virtual/commit/843109c5bf780591a762f9767f3808fd15e3f94e)]: - @tanstack/virtual-core@3.13.19 ## 3.13.18 ### Patch Changes - Updated dependencies [[`9067574`](https://github.com/TanStack/virtual/commit/9067574f1a0178d30e27bcac70853bdcbf437fec)]: - @tanstack/virtual-core@3.13.18 ## 3.13.17 ### Patch Changes - Updated dependencies [[`21d9a46`](https://github.com/TanStack/virtual/commit/21d9a46eac034cb4299872891694965bceed526d)]: - @tanstack/virtual-core@3.13.17 ## 3.13.16 ### Patch Changes - Updated dependencies [[`db6df21`](https://github.com/TanStack/virtual/commit/db6df212ed83dd7e4eb6450d1340c95475667b7b)]: - @tanstack/virtual-core@3.13.16 ## 3.13.15 ### Patch Changes - Updated dependencies [[`5a273bf`](https://github.com/TanStack/virtual/commit/5a273bf0c0bc0255ca172929f021c3b6e50cb69d)]: - @tanstack/virtual-core@3.13.15 ## 3.13.14 ### Patch Changes - Updated dependencies [[`6d9274c`](https://github.com/TanStack/virtual/commit/6d9274c3f0a9e64450b5829872079a65277bc654)]: - @tanstack/virtual-core@3.13.14 ## 3.13.13 ### Patch Changes - Fix: Notify framework when count changes to update getTotalSize() ([#1085](https://github.com/TanStack/virtual/pull/1085)) Fixed an issue where `getTotalSize()` would return stale values when the `count` option changed (e.g., during filtering or search operations). The virtualizer now automatically notifies the framework when measurement-affecting options change, ensuring the UI updates correctly without requiring manual `useMemo` workarounds. **Before**: When filtering items, the list container would maintain its previous height, causing excessive blank space (when count decreased) or inaccessible items (when count increased). **After**: Height updates automatically when count changes, providing the correct user experience. This fix applies to all framework adapters and has minimal performance impact (< 0.1ms per change). - Updated dependencies [[`2542c5a`](https://github.com/TanStack/virtual/commit/2542c5a3d6820cea956fa3b4f94c42e3526a8d68), [`96e32a6`](https://github.com/TanStack/virtual/commit/96e32a6ffc125743a0172ea4e0fe37ac29c4187b)]: - @tanstack/virtual-core@3.13.13 ## 3.13.12 ### Patch Changes - Updated dependencies [[`d21ed98`](https://github.com/TanStack/virtual/commit/d21ed98da3470b9986c9a028ed70fdf0d6189ab4)]: - @tanstack/virtual-core@3.13.12 ## 3.13.11 ### Patch Changes - Updated dependencies [[`73fa867`](https://github.com/TanStack/virtual/commit/73fa86752599a4bffba51ec8e4ff2f8cb8283010)]: - @tanstack/virtual-core@3.13.11 ## 3.13.10 ### Patch Changes - Updated dependencies [[`b3b7e7d`](https://github.com/TanStack/virtual/commit/b3b7e7dc8b25daeebbd2da61b3b7ae3448babbdb)]: - @tanstack/virtual-core@3.13.10 ## 3.13.9 ### Patch Changes - Updated dependencies [[`9e33cdb`](https://github.com/TanStack/virtual/commit/9e33cdb1c8780c2f455aafc11a0aeea58b71fc69)]: - @tanstack/virtual-core@3.13.9 ## 3.13.8 ### Patch Changes - Updated dependencies [[`60719f6`](https://github.com/TanStack/virtual/commit/60719f61b589d6f9d886e4f7c093217f6d693faf)]: - @tanstack/virtual-core@3.13.8 ## 3.13.7 ### Patch Changes - Updated dependencies [[`e2d93c2`](https://github.com/TanStack/virtual/commit/e2d93c2dcde9ccf60f658e56edccd8d05aefeee6)]: - @tanstack/virtual-core@3.13.7 ## 3.13.6 ### Patch Changes - Updated dependencies [[`042616f`](https://github.com/TanStack/virtual/commit/042616f39ced842470db0b4b40fca77f22454b7f)]: - @tanstack/virtual-core@3.13.6 ## 3.13.5 ### Patch Changes - Updated dependencies [[`51656d9`](https://github.com/TanStack/virtual/commit/51656d94a2469a065e631f25ffc8ec0288d9f5ec)]: - @tanstack/virtual-core@3.13.5 ## 3.13.4 ### Patch Changes - Updated dependencies [[`514b62d`](https://github.com/TanStack/virtual/commit/514b62d04974c2fd59fc8a68ed40f4c1a1547dd2), [`f03d814`](https://github.com/TanStack/virtual/commit/f03d8142c03ea0f5816161a4dad38ca35469841c)]: - @tanstack/virtual-core@3.13.4 ## 3.13.3 ### Patch Changes - Updated dependencies [[`02ef309`](https://github.com/TanStack/virtual/commit/02ef3097de4a14ed4077ace2ca901dc411bf81c1)]: - @tanstack/virtual-core@3.13.3 ================================================ FILE: packages/vue-virtual/eslint.config.js ================================================ // @ts-check import rootConfig from '../../eslint.config.js' export default [...rootConfig] ================================================ FILE: packages/vue-virtual/package.json ================================================ { "name": "@tanstack/vue-virtual", "version": "3.13.23", "description": "Headless UI for virtualizing scrollable elements in Vue", "author": "Tanner Linsley", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/TanStack/virtual.git", "directory": "packages/vue-virtual" }, "homepage": "https://tanstack.com/virtual", "publishConfig": { "registry": "https://registry.npmjs.org/" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "keywords": [ "react", "vue", "solid", "svelte", "virtual", "virtual-core", "datagrid" ], "scripts": { "clean": "premove ./dist ./coverage", "test:eslint": "eslint ./src", "test:types": "tsc", "test:build": "publint --strict", "build": "vite build" }, "type": "module", "types": "dist/esm/index.d.ts", "main": "dist/cjs/index.cjs", "module": "dist/esm/index.js", "exports": { ".": { "import": { "types": "./dist/esm/index.d.ts", "default": "./dist/esm/index.js" }, "require": { "types": "./dist/cjs/index.d.cts", "default": "./dist/cjs/index.cjs" } }, "./package.json": "./package.json" }, "sideEffects": false, "files": [ "dist", "src" ], "dependencies": { "@tanstack/virtual-core": "workspace:*" }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.4", "vue": "^3.5.16" }, "peerDependencies": { "vue": "^2.7.0 || ^3.0.0" } } ================================================ FILE: packages/vue-virtual/src/index.ts ================================================ import { Virtualizer, elementScroll, observeElementOffset, observeElementRect, observeWindowOffset, observeWindowRect, windowScroll, } from '@tanstack/virtual-core' import { computed, onScopeDispose, shallowRef, triggerRef, unref, watch, } from 'vue' import type { PartialKeys, VirtualizerOptions } from '@tanstack/virtual-core' import type { Ref } from 'vue' export * from '@tanstack/virtual-core' type MaybeRef = T | Ref function useVirtualizerBase< TScrollElement extends Element | Window, TItemElement extends Element, >( options: MaybeRef>, ): Ref> { const virtualizer = new Virtualizer(unref(options)) const state = shallowRef(virtualizer) const cleanup = virtualizer._didMount() watch( () => unref(options).getScrollElement(), (el) => { if (el) { virtualizer._willUpdate() } }, { immediate: true, }, ) watch( () => unref(options), (options) => { virtualizer.setOptions({ ...options, onChange: (instance, sync) => { triggerRef(state) options.onChange?.(instance, sync) }, }) virtualizer._willUpdate() triggerRef(state) }, { immediate: true, }, ) onScopeDispose(cleanup) return state } export function useVirtualizer< TScrollElement extends Element, TItemElement extends Element, >( options: MaybeRef< PartialKeys< VirtualizerOptions, 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' > >, ): Ref> { return useVirtualizerBase( computed(() => ({ observeElementRect: observeElementRect, observeElementOffset: observeElementOffset, scrollToFn: elementScroll, ...unref(options), })), ) } export function useWindowVirtualizer( options: MaybeRef< PartialKeys< VirtualizerOptions, | 'observeElementRect' | 'observeElementOffset' | 'scrollToFn' | 'getScrollElement' > >, ): Ref> { return useVirtualizerBase( computed(() => ({ getScrollElement: () => (typeof document !== 'undefined' ? window : null), observeElementRect: observeWindowRect, observeElementOffset: observeWindowOffset, scrollToFn: windowScroll, initialOffset: () => typeof document !== 'undefined' ? window.scrollY : 0, ...unref(options), })), ) } ================================================ FILE: packages/vue-virtual/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src", "eslint.config.js", "vite.config.ts"] } ================================================ FILE: packages/vue-virtual/vite.config.ts ================================================ import { defineConfig, mergeConfig } from 'vitest/config' import { tanstackViteConfig } from '@tanstack/vite-config' import vue from '@vitejs/plugin-vue' const config = defineConfig({ plugins: [vue()], }) export default mergeConfig( config, tanstackViteConfig({ entry: './src/index.ts', srcDir: './src', }), ) ================================================ FILE: pnpm-workspace.yaml ================================================ cleanupUnusedCatalogs: true linkWorkspacePackages: true preferWorkspacePackages: true packages: - 'packages/*' - 'examples/angular/*' - 'examples/react/*' - 'examples/svelte/*' - 'examples/vue/*' - 'examples/lit/*' ================================================ FILE: prettier.config.js ================================================ // @ts-check /** @type {import('prettier').Config} */ const config = { semi: false, singleQuote: true, trailingComma: 'all', plugins: ['prettier-plugin-svelte'], overrides: [{ files: '*.svelte', options: { parser: 'svelte' } }], } export default config ================================================ FILE: scripts/verify-links.ts ================================================ import { existsSync, readFileSync, statSync } from 'node:fs' import { extname, resolve } from 'node:path' import { glob } from 'tinyglobby' // @ts-ignore Could not find a declaration file for module 'markdown-link-extractor'. import markdownLinkExtractor from 'markdown-link-extractor' const errors: Array<{ file: string link: string resolvedPath: string reason: string }> = [] function isRelativeLink(link: string) { return ( !link.startsWith('/') && !link.startsWith('http://') && !link.startsWith('https://') && !link.startsWith('//') && !link.startsWith('#') && !link.startsWith('mailto:') ) } /** Remove any trailing .md */ function stripExtension(p: string): string { return p.replace(`${extname(p)}`, '') } function relativeLinkExists(link: string, file: string): boolean { // Remove hash if present const linkWithoutHash = link.split('#')[0] // If the link is empty after removing hash, it's not a file if (!linkWithoutHash) return false // Strip the file/link extensions const filePath = stripExtension(file) const linkPath = stripExtension(linkWithoutHash) // Resolve the path relative to the markdown file's directory // Nav up a level to simulate how links are resolved on the web let absPath = resolve(filePath, '..', linkPath) // Ensure the resolved path is within /docs const docsRoot = resolve('docs') if (!absPath.startsWith(docsRoot)) { errors.push({ link, file, resolvedPath: absPath, reason: 'Path outside /docs', }) return false } // Check if this is an example path const isExample = absPath.includes('/examples/') let exists = false if (isExample) { // Transform /docs/framework/{framework}/examples/ to /examples/{framework}/ absPath = absPath.replace( /\/docs\/framework\/([^/]+)\/examples\//, '/examples/$1/', ) // For examples, we want to check if the directory exists exists = existsSync(absPath) && statSync(absPath).isDirectory() } else { // For non-examples, we want to check if the .md file exists if (!absPath.endsWith('.md')) { absPath = `${absPath}.md` } exists = existsSync(absPath) } if (!exists) { errors.push({ link, file, resolvedPath: absPath, reason: 'Not found', }) } return exists } async function verifyMarkdownLinks() { // Find all markdown files in docs directory const markdownFiles = await glob('docs/**/*.md', { ignore: ['**/node_modules/**'], }) console.log(`Found ${markdownFiles.length} markdown files\n`) // Process each file for (const file of markdownFiles) { const content = readFileSync(file, 'utf-8') const links: Array = markdownLinkExtractor(content) const relativeLinks = links.filter((link: string) => { return isRelativeLink(link) }) if (relativeLinks.length > 0) { relativeLinks.forEach((link) => { relativeLinkExists(link, file) }) } } if (errors.length > 0) { console.log(`\n❌ Found ${errors.length} broken links:`) errors.forEach((err) => { console.log( `${err.file}\n link: ${err.link}\n resolved: ${err.resolvedPath}\n why: ${err.reason}\n`, ) }) process.exit(1) } else { console.log('\n✅ No broken links found!') } } verifyMarkdownLinks().catch(console.error) ================================================ FILE: tsconfig.json ================================================ { "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "allowJs": true, "allowSyntheticDefaultImports": true, "allowUnreachableCode": false, "allowUnusedLabels": false, "checkJs": true, "declaration": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, "lib": ["DOM", "DOM.Iterable", "ES2022"], "module": "ESNext", "moduleResolution": "Bundler", "noEmit": true, "noImplicitReturns": true, "noUncheckedIndexedAccess": true, "noUnusedLocals": false, "noUnusedParameters": false, "resolveJsonModule": true, "skipLibCheck": true, "strict": true, "target": "ES2020" }, "include": ["*.config.*", "scripts"] }