Repository: tailwindlabs/headlessui
Pull request #3697 (2025-04-17 09:23:22)

Improve `Combobox` component performance
This PR improves the performance of the `Combobox` component. This is a similar implementation as:

- https://github.com/tailwindlabs/headlessui/pull/3685
- https://github.com/tailwindlabs/headlessui/pull/3688

Before this PR, the `Combobox` component is built in a way where all the state lives in the `Combobox` itself. If state changes, everything re-renders and re-computes the necessary derived state.

However, if you have a 1000 items, then every time the active item changes, all 1000 items have to re-render.

To solve this, we can move the state outside of the `Combobox` component, and "subscribe" to state changes using the `useSlice` hook introduced in https://github.com/tailwindlabs/headlessui/pull/3684.

This will allow us to subscribe to a slice of the state, and only re-render if the computed slice actually changes.

If the active item changes, only 3 things will happen:

1. The `ComboboxOptions` will re-render and have an updated `aria-activedescendant`
2. The `ComboboxOption` that _was_ active, will re-render and the `data-focus` attribute wil be removed.
3. The `ComboboxOption` that is now active, will re-render and the `data-focus` attribute wil be added.

The `Combobox` component already has a `virtual` option if you want to render many many more items. This is a bit of a different model where all the options are passed in via an array instead of rendering all `ComboboxOption` components immediately.

Because of this, I didn't want to batch the registration of the options as part of this PR (similar to what we do in the `Menu` and `Listbox`) because it behaves differently compared to what mode you are using (virtual or not). Since not all components will be rendered, batching the registration until everything is registered doesn't really make sense in the general case. However, it does make sense in non-virtual mode. But because of this difference, I didn't want to implement this as part of this PR and increase the complexity of the PR even more.

Instead I will follow up with more PRs with more improvements. But the key improvement of looking at the slice of the data is what makes the biggest impact. This also means that we can do another release once this is merged.

Last but not least, recently we fixed a bug where the `Combobox` in `virtual` mode could crash if you search for an item that doesn't exist. To solve it, we implemented a workaround in:

- https://github.com/tailwindlabs/headlessui/pull/3678

Which used a double `requestAnimationFrame` to delay the scrolling to the item. While this solved this issue, this also caused visual flicker when holding down your arrow keys.

I also fixed it in this PR by introducing `patch-package` and work around the issue in the `@tanstack/virtual-core` package itself.

More info: 96f4da70b16d5cf259643

Before:

https://github.com/user-attachments/assets/132520d3-b4d6-42f9-9152-57427de20639

After:

https://github.com/user-attachments/assets/41f198fe-9326-42d1-a09f-077b60a9f65d

## Test plan

1. All tests still pass
2. Tested this in the browser with a 1000 items. In the videos below the only thing I'm doing is holding down the `ArrowDown` key.

Before:

https://github.com/user-attachments/assets/945692a3-96e6-4ac7-bee0-36a1fd89172b

After:

https://github.com/user-attachments/assets/98a151d0-16cc-4823-811c-fcee0019937a


Patch:
diff --git a/package-lock.json b/package-lock.json
index 1368bc4..b7da196 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,6 +7,7 @@
     "": {
       "name": "headlessui",
       "version": "0.0.0",
+      "hasInstallScript": true,
       "license": "MIT",
       "workspaces": [
         "packages/*",
@@ -25,6 +26,7 @@
         "jest": "26",
         "lint-staged": "^12.2.1",
         "npm-run-all": "^4.1.5",
+        "patch-package": "^8.0.0",
         "prettier": "^3.1.0",
         "prettier-plugin-organize-imports": "^3.2.4",
         "prettier-plugin-tailwindcss": "^0.6.11",
@@ -2638,12 +2640,12 @@
       }
     },
     "node_modules/@tanstack/react-virtual": {
-      "version": "3.11.1",
-      "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.1.tgz",
-      "integrity": "sha512-orn2QNe5tF6SqjucHJ6cKTKcRDe3GG7bcYqPNn72Yejj7noECdzgAyRfGt2pGDPemhYim3d1HIR/dgruCnLfUA==",
+      "version": "3.13.6",
+      "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.6.tgz",
+      "integrity": "sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==",
       "license": "MIT",
       "dependencies": {
-        "@tanstack/virtual-core": "3.10.9"
+        "@tanstack/virtual-core": "3.13.6"
       },
       "funding": {
         "type": "github",
@@ -2654,22 +2656,30 @@
         "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
       }
     },
-    "node_modules/@tanstack/react-virtual/node_modules/@tanstack/virtual-core": {
-      "version": "3.10.9",
-      "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.9.tgz",
-      "integrity": "sha512-kBknKOKzmeR7lN+vSadaKWXaLS0SZZG+oqpQ/k80Q6g9REn6zRHS/ZYdrIzHnpHgy/eWs00SujveUN/GJT2qTw==",
+    "node_modules/@tanstack/virtual-core": {
+      "version": "3.13.6",
+      "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.6.tgz",
+      "integrity": "sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==",
       "license": "MIT",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/tannerlinsley"
       }
     },
-    "node_modules/@tanstack/virtual-core": {
-      "version": "3.0.0-beta.60",
+    "node_modules/@tanstack/vue-virtual": {
+      "version": "3.13.6",
+      "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.6.tgz",
+      "integrity": "sha512-GYdZ3SJBQPzgxhuCE2fvpiH46qzHiVx5XzBSdtESgiqh4poj8UgckjGWYEhxaBbcVt1oLzh1m3Ql4TyH32TOzQ==",
       "license": "MIT",
+      "dependencies": {
+        "@tanstack/virtual-core": "3.13.6"
+      },
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/tannerlinsley"
+      },
+      "peerDependencies": {
+        "vue": "^2.7.0 || ^3.0.0"
       }
     },
     "node_modules/@testing-library/dom": {
@@ -3141,6 +3151,7 @@
     },
     "node_modules/@vue/compiler-core": {
       "version": "3.2.37",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@babel/parser": "^7.16.4",
@@ -3151,6 +3162,7 @@
     },
     "node_modules/@vue/compiler-dom": {
       "version": "3.2.37",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@vue/compiler-core": "3.2.37",
@@ -3218,6 +3230,7 @@
     },
     "node_modules/@vue/compiler-ssr": {
       "version": "3.2.37",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@vue/compiler-dom": "3.2.37",
@@ -3238,6 +3251,7 @@
     },
     "node_modules/@vue/reactivity-transform": {
       "version": "3.2.37",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@babel/parser": "^7.16.4",
@@ -3330,8 +3344,16 @@
     },
     "node_modules/@vue/shared": {
       "version": "3.2.37",
+      "dev": true,
       "license": "MIT"
     },
+    "node_modules/@yarnpkg/lockfile": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
+      "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
+      "dev": true,
+      "license": "BSD-2-Clause"
+    },
     "node_modules/abab": {
       "version": "2.0.6",
       "dev": true,
@@ -3577,6 +3599,16 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/at-least-node": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+      "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
     "node_modules/atob": {
       "version": "2.1.2",
       "dev": true,
@@ -3858,13 +3890,50 @@
       }
     },
     "node_modules/call-bind": {
-      "version": "1.0.5",
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+      "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "function-bind": "^1.1.2",
-        "get-intrinsic": "^1.2.1",
-        "set-function-length": "^1.1.1"
+        "call-bind-apply-helpers": "^1.0.0",
+        "es-define-property": "^1.0.0",
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -4405,16 +4474,21 @@
       }
     },
     "node_modules/define-data-property": {
-      "version": "1.1.1",
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+      "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "get-intrinsic": "^1.2.1",
-        "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.0"
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
       },
       "engines": {
         "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/define-properties": {
@@ -4529,6 +4603,21 @@
         "node": ">=8"
       }
     },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/eastasianwidth": {
       "version": "0.2.0",
       "license": "MIT"
@@ -4681,6 +4770,26 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/es-get-iterator": {
       "version": "1.1.3",
       "dev": true,
@@ -4700,6 +4809,19 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/es-set-tostringtag": {
       "version": "2.0.2",
       "dev": true,
@@ -5364,6 +5486,16 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/find-yarn-workspace-root": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
+      "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "micromatch": "^4.0.2"
+      }
+    },
     "node_modules/flatpickr": {
       "version": "4.6.13",
       "license": "MIT"
@@ -5458,6 +5590,32 @@
         "tslib": "^2.1.0"
       }
     },
+    "node_modules/fs-extra": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+      "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "at-least-node": "^1.0.0",
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/fs-extra/node_modules/universalify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+      "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
     "node_modules/fs.realpath": {
       "version": "1.0.0",
       "dev": true,
@@ -5523,14 +5681,25 @@
       }
     },
     "node_modules/get-intrinsic": {
-      "version": "1.2.2",
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
         "function-bind": "^1.1.2",
-        "has-proto": "^1.0.1",
-        "has-symbols": "^1.0.3",
-        "hasown": "^2.0.0"
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -5544,6 +5713,20 @@
         "node": ">=8.0.0"
       }
     },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/get-stream": {
       "version": "6.0.1",
       "dev": true,
@@ -5641,11 +5824,13 @@
       }
     },
     "node_modules/gopd": {
-      "version": "1.0.1",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
       "dev": true,
       "license": "MIT",
-      "dependencies": {
-        "get-intrinsic": "^1.1.3"
+      "engines": {
+        "node": ">= 0.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -5678,11 +5863,13 @@
       }
     },
     "node_modules/has-property-descriptors": {
-      "version": "1.0.1",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+      "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "get-intrinsic": "^1.2.2"
+        "es-define-property": "^1.0.0"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
@@ -5700,7 +5887,9 @@
       }
     },
     "node_modules/has-symbols": {
-      "version": "1.0.3",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -5761,7 +5950,9 @@
       }
     },
     "node_modules/hasown": {
-      "version": "2.0.0",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
       "license": "MIT",
       "dependencies": {
         "function-bind": "^1.1.2"
@@ -6126,7 +6317,6 @@
       "version": "2.2.1",
       "dev": true,
       "license": "MIT",
-      "optional": true,
       "bin": {
         "is-docker": "cli.js"
       },
@@ -6372,7 +6562,6 @@
       "version": "2.2.0",
       "dev": true,
       "license": "MIT",
-      "optional": true,
       "dependencies": {
         "is-docker": "^2.0.0"
       },
@@ -7289,6 +7478,26 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/json-stable-stringify": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.2.1.tgz",
+      "integrity": "sha512-Lp6HbbBgosLmJbjx0pBLbgvx68FaFU1sdkmBuckmhhJ88kL13OA51CDtR2yJB50eCNMH9wRqtQNNiAqQH4YXnA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "isarray": "^2.0.5",
+        "jsonify": "^0.0.1",
+        "object-keys": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/json5": {
       "version": "2.2.3",
       "dev": true,
@@ -7305,6 +7514,39 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/jsonfile": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+      "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "universalify": "^2.0.0"
+      },
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "node_modules/jsonfile/node_modules/universalify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+      "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
+    "node_modules/jsonify": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
+      "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
+      "dev": true,
+      "license": "Public Domain",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/kind-of": {
       "version": "3.2.2",
       "dev": true,
@@ -7316,6 +7558,16 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/klaw-sync": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
+      "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.1.11"
+      }
+    },
     "node_modules/kleur": {
       "version": "3.0.3",
       "dev": true,
@@ -7820,6 +8072,7 @@
     },
     "node_modules/magic-string": {
       "version": "0.25.9",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "sourcemap-codec": "^1.4.8"
@@ -7932,6 +8185,16 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/memorystream": {
       "version": "0.3.1",
       "dev": true,
@@ -8600,6 +8863,23 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/open": {
+      "version": "7.4.2",
+      "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
+      "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-docker": "^2.0.0",
+        "is-wsl": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/opencollective-postinstall": {
       "version": "2.0.3",
       "dev": true,
@@ -8608,6 +8888,16 @@
         "opencollective-postinstall": "index.js"
       }
     },
+    "node_modules/os-tmpdir": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+      "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/p-each-series": {
       "version": "2.2.0",
       "dev": true,
@@ -8715,6 +9005,90 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/patch-package": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz",
+      "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@yarnpkg/lockfile": "^1.1.0",
+        "chalk": "^4.1.2",
+        "ci-info": "^3.7.0",
+        "cross-spawn": "^7.0.3",
+        "find-yarn-workspace-root": "^2.0.0",
+        "fs-extra": "^9.0.0",
+        "json-stable-stringify": "^1.0.2",
+        "klaw-sync": "^6.0.0",
+        "minimist": "^1.2.6",
+        "open": "^7.4.2",
+        "rimraf": "^2.6.3",
+        "semver": "^7.5.3",
+        "slash": "^2.0.0",
+        "tmp": "^0.0.33",
+        "yaml": "^2.2.2"
+      },
+      "bin": {
+        "patch-package": "index.js"
+      },
+      "engines": {
+        "node": ">=14",
+        "npm": ">5"
+      }
+    },
+    "node_modules/patch-package/node_modules/ci-info": {
+      "version": "3.9.0",
+      "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
+      "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/sibiraj-s"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/patch-package/node_modules/rimraf": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+      "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+      "deprecated": "Rimraf versions prior to v4 are no longer supported",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      }
+    },
+    "node_modules/patch-package/node_modules/slash": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
+      "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/patch-package/node_modules/yaml": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz",
+      "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "yaml": "bin.mjs"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
     "node_modules/path-exists": {
       "version": "4.0.0",
       "dev": true,
@@ -10029,14 +10403,18 @@
       "license": "ISC"
     },
     "node_modules/set-function-length": {
-      "version": "1.1.1",
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+      "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "define-data-property": "^1.1.1",
-        "get-intrinsic": "^1.2.1",
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
         "gopd": "^1.0.1",
-        "has-property-descriptors": "^1.0.0"
+        "has-property-descriptors": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
@@ -10586,6 +10964,7 @@
     },
     "node_modules/source-map": {
       "version": "0.6.1",
+      "dev": true,
       "license": "BSD-3-Clause",
       "engines": {
         "node": ">=0.10.0"
@@ -10628,6 +11007,7 @@
     },
     "node_modules/sourcemap-codec": {
       "version": "1.4.8",
+      "dev": true,
       "license": "MIT"
     },
     "node_modules/spdx-correct": {
@@ -11163,6 +11543,19 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/tmp": {
+      "version": "0.0.33",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+      "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "os-tmpdir": "~1.0.2"
+      },
+      "engines": {
+        "node": ">=0.6.0"
+      }
+    },
     "node_modules/tmpl": {
       "version": "1.0.5",
       "dev": true,
@@ -11997,7 +12390,7 @@
         "@floating-ui/react": "^0.26.16",
         "@react-aria/focus": "^3.17.1",
         "@react-aria/interactions": "^3.21.3",
-        "@tanstack/react-virtual": "^3.11.1",
+        "@tanstack/react-virtual": "^3.13.6",
         "use-sync-external-store": "^1.5.0"
       },
       "devDependencies": {
@@ -12056,7 +12449,7 @@
       "version": "1.7.22",
       "license": "MIT",
       "dependencies": {
-        "@tanstack/vue-virtual": "3.0.0-beta.60"
+        "@tanstack/vue-virtual": "3.13.6"
       },
       "devDependencies": {
         "@testing-library/vue": "8.0.0",
@@ -12070,20 +12463,6 @@
         "vue": "^3.2.0"
       }
     },
-    "packages/@headlessui-vue/node_modules/@tanstack/vue-virtual": {
-      "version": "3.0.0-beta.60",
-      "license": "MIT",
-      "dependencies": {
-        "@tanstack/virtual-core": "3.0.0-beta.60"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/tannerlinsley"
-      },
-      "peerDependencies": {
-        "vue": "^2.7.0 || ^3.0.0"
-      }
-    },
     "packages/@headlessui-vue/node_modules/@testing-library/dom": {
       "version": "9.3.3",
       "dev": true,
@@ -12121,6 +12500,7 @@
     },
     "packages/@headlessui-vue/node_modules/@vue/compiler-sfc": {
       "version": "3.2.37",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@babel/parser": "^7.16.4",
@@ -12137,6 +12517,7 @@
     },
     "packages/@headlessui-vue/node_modules/@vue/reactivity": {
       "version": "3.2.37",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@vue/shared": "3.2.37"
@@ -12144,6 +12525,7 @@
     },
     "packages/@headlessui-vue/node_modules/@vue/runtime-core": {
       "version": "3.2.37",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@vue/reactivity": "3.2.37",
@@ -12152,6 +12534,7 @@
     },
     "packages/@headlessui-vue/node_modules/@vue/runtime-dom": {
       "version": "3.2.37",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@vue/runtime-core": "3.2.37",
@@ -12161,6 +12544,7 @@
     },
     "packages/@headlessui-vue/node_modules/@vue/server-renderer": {
       "version": "3.2.37",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@vue/compiler-ssr": "3.2.37",
@@ -12201,6 +12585,7 @@
     },
     "packages/@headlessui-vue/node_modules/csstype": {
       "version": "2.6.21",
+      "dev": true,
       "license": "MIT"
     },
     "packages/@headlessui-vue/node_modules/pretty-format": {
@@ -12218,6 +12603,7 @@
     },
     "packages/@headlessui-vue/node_modules/vue": {
       "version": "3.2.37",
+      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@vue/compiler-dom": "3.2.37",
diff --git a/package.json b/package.json
index 9d75aa7..9c87b7d 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,8 @@
     "lint-types": "CI=true npm run lint-types --workspaces --if-present",
     "release-channel": "node ./scripts/release-channel.js",
     "release-notes": "node ./scripts/release-notes.js",
-    "package-path": "node ./scripts/package-path.js"
+    "package-path": "node ./scripts/package-path.js",
+    "postinstall": "patch-package"
   },
   "husky": {
     "hooks": {
@@ -76,6 +77,7 @@
     "jest": "26",
     "lint-staged": "^12.2.1",
     "npm-run-all": "^4.1.5",
+    "patch-package": "^8.0.0",
     "prettier": "^3.1.0",
     "prettier-plugin-organize-imports": "^3.2.4",
     "prettier-plugin-tailwindcss": "^0.6.11",
diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md
index da29474..e3d45a0 100644
--- a/packages/@headlessui-react/CHANGELOG.md
+++ b/packages/@headlessui-react/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Improve `Listbox` component performance ([#3688](https://github.com/tailwindlabs/headlessui/pull/3688))
 - Open `Menu` and `Listbox` on `mousedown` ([#3689](https://github.com/tailwindlabs/headlessui/pull/3689))
 - Fix `Transition` component from incorrectly exposing the `Closing` state ([#3696](https://github.com/tailwindlabs/headlessui/pull/3696))
+- Improve `Combobox` component performance ([#3697](https://github.com/tailwindlabs/headlessui/pull/3697))
 
 ## [2.2.1] - 2025-04-04
 
diff --git a/packages/@headlessui-react/package.json b/packages/@headlessui-react/package.json
index 9e02672..98d0c69 100644
--- a/packages/@headlessui-react/package.json
+++ b/packages/@headlessui-react/package.json
@@ -59,7 +59,7 @@
     "@floating-ui/react": "^0.26.16",
     "@react-aria/focus": "^3.17.1",
     "@react-aria/interactions": "^3.21.3",
-    "@tanstack/react-virtual": "^3.11.1",
+    "@tanstack/react-virtual": "^3.13.6",
     "use-sync-external-store": "^1.5.0"
   }
 }
diff --git a/packages/@headlessui-react/src/components/combobox/combobox-machine-glue.tsx b/packages/@headlessui-react/src/components/combobox/combobox-machine-glue.tsx
new file mode 100644
index 0000000..4121fbe
--- /dev/null
+++ b/packages/@headlessui-react/src/components/combobox/combobox-machine-glue.tsx
@@ -0,0 +1,20 @@
+import { createContext, useContext, useMemo } from 'react'
+import { ComboboxMachine } from './combobox-machine'
+
+export const ComboboxContext = createContext<ComboboxMachine<unknown> | null>(null)
+export function useComboboxMachineContext<T>(component: string) {
+  let context = useContext(ComboboxContext)
+  if (context === null) {
+    let err = new Error(`<${component} /> is missing a parent <Combobox /> component.`)
+    if (Error.captureStackTrace) Error.captureStackTrace(err, useComboboxMachine)
+    throw err
+  }
+  return context as ComboboxMachine<T>
+}
+
+export function useComboboxMachine({
+  virtual = null,
+  __demoMode = false,
+}: Parameters<typeof ComboboxMachine.new>[0] = {}) {
+  return useMemo(() => ComboboxMachine.new({ virtual, __demoMode }), [])
+}
diff --git a/packages/@headlessui-react/src/components/combobox/combobox-machine.ts b/packages/@headlessui-react/src/components/combobox/combobox-machine.ts
new file mode 100644
index 0000000..aa6353c
--- /dev/null
+++ b/packages/@headlessui-react/src/components/combobox/combobox-machine.ts
@@ -0,0 +1,617 @@
+import { Machine } from '../../machine'
+import type { EnsureArray } from '../../types'
+import { Focus, calculateActiveIndex } from '../../utils/calculate-active-index'
+import { sortByDomNode } from '../../utils/focus-management'
+import { match } from '../../utils/match'
+
+interface MutableRefObject<T> {
+  current: T
+}
+
+export enum ComboboxState {
+  Open,
+  Closed,
+}
+
+export enum ValueMode {
+  Single,
+  Multi,
+}
+
+export enum ActivationTrigger {
+  Pointer,
+  Focus,
+  Other,
+}
+
+export type ComboboxOptionDataRef<T> = MutableRefObject<{
+  disabled: boolean
+  value: T
+  domRef: MutableRefObject<HTMLElement | null>
+  order: number | null
+}>
+
+export interface State<T> {
+  dataRef: MutableRefObject<{
+    value: unknown
+    defaultValue: unknown
+    disabled: boolean
+    invalid: boolean
+    mode: ValueMode
+    immediate: boolean
+    onChange: (value: T) => void
+    onClose?: () => void
+    compare(a: unknown, z: unknown): boolean
+    isSelected(value: unknown): boolean
+
+    virtual: { options: T[]; disabled: (value: T) => boolean } | null
+    calculateIndex(value: unknown): number
+
+    __demoMode: boolean
+
+    optionsPropsRef: MutableRefObject<{
+      static: boolean
+      hold: boolean
+    }>
+  }>
+
+  virtual: { options: T[]; disabled: (value: unknown) => boolean } | null
+
+  comboboxState: ComboboxState
+
+  defaultToFirstOption: boolean
+
+  options: { id: string; dataRef: ComboboxOptionDataRef<T> }[]
+  activeOptionIndex: number | null
+  activationTrigger: ActivationTrigger
+
+  isTyping: boolean
+
+  inputElement: HTMLInputElement | null
+  buttonElement: HTMLButtonElement | null
+  optionsElement: HTMLElement | null
+
+  __demoMode: boolean
+}
+
+export enum ActionTypes {
+  OpenCombobox,
+  CloseCombobox,
+
+  GoToOption,
+  SetTyping,
+
+  RegisterOption,
+  UnregisterOption,
+
+  DefaultToFirstOption,
+
+  SetActivationTrigger,
+
+  UpdateVirtualConfiguration,
+
+  SetInputElement,
+  SetButtonElement,
+  SetOptionsElement,
+}
+
+function adjustOrderedState<T>(
+  state: State<T>,
+  adjustment: (options: State<T>['options']) => State<T>['options'] = (i) => i
+) {
+  let currentActiveOption =
+    state.activeOptionIndex !== null ? state.options[state.activeOptionIndex] : null
+
+  let list = adjustment(state.options.slice())
+  let sortedOptions =
+    list.length > 0 && list[0].dataRef.current.order !== null
+      ? // Prefer sorting based on the `order`
+        list.sort((a, z) => a.dataRef.current.order! - z.dataRef.current.order!)
+      : // Fallback to much slower DOM order
+        sortByDomNode(list, (option) => option.dataRef.current.domRef.current)
+
+  // If we inserted an option before the current active option then the active option index
+  // would be wrong. To fix this, we will re-lookup the correct index.
+  let adjustedActiveOptionIndex = currentActiveOption
+    ? sortedOptions.indexOf(currentActiveOption)
+    : null
+
+  // Reset to `null` in case the currentActiveOption was removed.
+  if (adjustedActiveOptionIndex === -1) {
+    adjustedActiveOptionIndex = null
+  }
+
+  return {
+    options: sortedOptions,
+    activeOptionIndex: adjustedActiveOptionIndex,
+  }
+}
+
+type Actions<T> =
+  | { type: ActionTypes.CloseCombobox }
+  | { type: ActionTypes.OpenCombobox }
+  | {
+      type: ActionTypes.GoToOption
+      focus: Focus.Specific
+      idx: number
+      trigger?: ActivationTrigger
+    }
+  | { type: ActionTypes.SetTyping; isTyping: boolean }
+  | {
+      type: ActionTypes.GoToOption
+      focus: Exclude<Focus, Focus.Specific>
+      trigger?: ActivationTrigger
+    }
+  | {
+      type: ActionTypes.RegisterOption
+      payload: { id: string; dataRef: ComboboxOptionDataRef<T> }
+    }
+  | { type: ActionTypes.UnregisterOption; id: string }
+  | { type: ActionTypes.DefaultToFirstOption; value: boolean }
+  | { type: ActionTypes.SetActivationTrigger; trigger: ActivationTrigger }
+  | {
+      type: ActionTypes.UpdateVirtualConfiguration
+      options: T[]
+      disabled: ((value: any) => boolean) | null
+    }
+  | { type: ActionTypes.SetInputElement; element: HTMLInputElement | null }
+  | { type: ActionTypes.SetButtonElement; element: HTMLButtonElement | null }
+  | { type: ActionTypes.SetOptionsElement; element: HTMLElement | null }
+
+let reducers: {
+  [P in ActionTypes]: <T>(state: State<T>, action: Extract<Actions<T>, { type: P }>) => State<T>
+} = {
+  [ActionTypes.CloseCombobox](state) {
+    if (state.dataRef.current?.disabled) return state
+    if (state.comboboxState === ComboboxState.Closed) return state
+
+    return {
+      ...state,
+      activeOptionIndex: null,
+      comboboxState: ComboboxState.Closed,
+
+      isTyping: false,
+
+      // Clear the last known activation trigger
+      // This is because if a user interacts with the combobox using a mouse
+      // resulting in it closing we might incorrectly handle the next interaction
+      // for example, not scrolling to the active option in a virtual list
+      activationTrigger: ActivationTrigger.Other,
+
+      __demoMode: false,
+    }
+  },
+  [ActionTypes.OpenCombobox](state) {
+    if (state.dataRef.current?.disabled) return state
+    if (state.comboboxState === ComboboxState.Open) return state
+
+    // Check if we have a selected value that we can make active
+    if (state.dataRef.current?.value) {
+      let idx = state.dataRef.current.calculateIndex(state.dataRef.current.value)
+      if (idx !== -1) {
+        return {
+          ...state,
+          activeOptionIndex: idx,
+          comboboxState: ComboboxState.Open,
+          __demoMode: false,
+        }
+      }
+    }
+
+    return { ...state, comboboxState: ComboboxState.Open, __demoMode: false }
+  },
+  [ActionTypes.SetTyping](state, action) {
+    if (state.isTyping === action.isTyping) return state
+    return { ...state, isTyping: action.isTyping }
+  },
+  [ActionTypes.GoToOption](state, action) {
+    if (state.dataRef.current?.disabled) return state
+    if (
+      state.optionsElement &&
+      !state.dataRef.current?.optionsPropsRef.current.static &&
+      state.comboboxState === ComboboxState.Closed
+    ) {
+      return state
+    }
+
+    if (state.virtual) {
+      let { options, disabled } = state.virtual
+      let activeOptionIndex =
+        action.focus === Focus.Specific
+          ? action.idx
+          : calculateActiveIndex(action, {
+              resolveItems: () => options,
+              resolveActiveIndex: () =>
+                state.activeOptionIndex ?? options.findIndex((option) => !disabled(option)) ?? null,
+              resolveDisabled: disabled,
+              resolveId() {
+                throw new Error('Function not implemented.')
+              },
+            })
+
+      let activationTrigger = action.trigger ?? ActivationTrigger.Other
+
+      if (
+        state.activeOptionIndex === activeOptionIndex &&
+        state.activationTrigger === activationTrigger
+      ) {
+        return state
+      }
+
+      return {
+        ...state,
+        activeOptionIndex,
+        activationTrigger,
+        isTyping: false,
+        __demoMode: false,
+      }
+    }
+
+    let adjustedState = adjustOrderedState(state)
+
+    // It's possible that the activeOptionIndex is set to `null` internally, but
+    // this means that we will fallback to the first non-disabled option by default.
+    // We have to take this into account.
+    if (adjustedState.activeOptionIndex === null) {
+      let localActiveOptionIndex = adjustedState.options.findIndex(
+        (option) => !option.dataRef.current.disabled
+      )
+
+      if (localActiveOptionIndex !== -1) {
+        adjustedState.activeOptionIndex = localActiveOptionIndex
+      }
+    }
+
+    let activeOptionIndex =
+      action.focus === Focus.Specific
+        ? action.idx
+        : calculateActiveIndex(action, {
+            resolveItems: () => adjustedState.options,
+            resolveActiveIndex: () => adjustedState.activeOptionIndex,
+            resolveId: (item) => item.id,
+            resolveDisabled: (item) => item.dataRef.current.disabled,
+          })
+    let activationTrigger = action.trigger ?? ActivationTrigger.Other
+
+    if (
+      state.activeOptionIndex === activeOptionIndex &&
+      state.activationTrigger === activationTrigger
+    ) {
+      return state
+    }
+
+    return {
+      ...state,
+      ...adjustedState,
+      isTyping: false,
+      activeOptionIndex,
+      activationTrigger,
+      __demoMode: false,
+    }
+  },
+  [ActionTypes.RegisterOption]: (state, action) => {
+    if (state.dataRef.current?.virtual) {
+      return {
+        ...state,
+        options: [...state.options, action.payload],
+      }
+    }
+
+    let option = action.payload
+
+    let adjustedState = adjustOrderedState(state, (options) => {
+      options.push(option)
+      return options
+    })
+
+    // Check if we need to make the newly registered option active.
+    if (state.activeOptionIndex === null) {
+      if (state.dataRef.current.isSelected?.(action.payload.dataRef.current.value)) {
+        adjustedState.activeOptionIndex = adjustedState.options.indexOf(option)
+      }
+    }
+
+    let nextState = {
+      ...state,
+      ...adjustedState,
+      activationTrigger: ActivationTrigger.Other,
+    }
+
+    if (state.dataRef.current?.__demoMode && state.dataRef.current.value === undefined) {
+      nextState.activeOptionIndex = 0
+    }
+
+    return nextState
+  },
+  [ActionTypes.UnregisterOption]: (state, action) => {
+    if (state.dataRef.current?.virtual) {
+      return {
+        ...state,
+        options: state.options.filter((option) => option.id !== action.id),
+      }
+    }
+
+    let adjustedState = adjustOrderedState(state, (options) => {
+      let idx = options.findIndex((option) => option.id === action.id)
+      if (idx !== -1) options.splice(idx, 1)
+      return options
+    })
+
+    return {
+      ...state,
+      ...adjustedState,
+      activationTrigger: ActivationTrigger.Other,
+    }
+  },
+  [ActionTypes.DefaultToFirstOption]: (state, action) => {
+    if (state.defaultToFirstOption === action.value) return state
+
+    return {
+      ...state,
+      defaultToFirstOption: action.value,
+    }
+  },
+  [ActionTypes.SetActivationTrigger]: (state, action) => {
+    if (state.activationTrigger === action.trigger) {
+      return state
+    }
+
+    return {
+      ...state,
+      activationTrigger: action.trigger,
+    }
+  },
+  [ActionTypes.UpdateVirtualConfiguration]: (state, action) => {
+    if (state.virtual === null) {
+      return {
+        ...state,
+        virtual: { options: action.options, disabled: action.disabled ?? (() => false) },
+      }
+    }
+
+    if (state.virtual.options === action.options && state.virtual.disabled === action.disabled) {
+      return state
+    }
+
+    let adjustedActiveOptionIndex = state.activeOptionIndex
+    if (state.activeOptionIndex !== null) {
+      let idx = action.options.indexOf(state.virtual.options[state.activeOptionIndex])
+      if (idx !== -1) {
+        adjustedActiveOptionIndex = idx
+      } else {
+        adjustedActiveOptionIndex = null
+      }
+    }
+
+    return {
+      ...state,
+      activeOptionIndex: adjustedActiveOptionIndex,
+      virtual: { options: action.options, disabled: action.disabled ?? (() => false) },
+    }
+  },
+  [ActionTypes.SetInputElement]: (state, action) => {
+    if (state.inputElement === action.element) return state
+    return { ...state, inputElement: action.element }
+  },
+  [ActionTypes.SetButtonElement]: (state, action) => {
+    if (state.buttonElement === action.element) return state
+    return { ...state, buttonElement: action.element }
+  },
+  [ActionTypes.SetOptionsElement]: (state, action) => {
+    if (state.optionsElement === action.element) return state
+    return { ...state, optionsElement: action.element }
+  },
+}
+
+export class ComboboxMachine<T> extends Machine<State<T>, Actions<T>> {
+  static new<T, TMultiple extends boolean | undefined>({
+    virtual = null,
+    __demoMode = false,
+  }: {
+    virtual?: {
+      options: TMultiple extends true ? EnsureArray<NoInfer<T>> : NoInfer<T>[]
+      disabled?: (
+        value: TMultiple extends true ? EnsureArray<NoInfer<T>>[number] : NoInfer<T>
+      ) => boolean
+    } | null
+    __demoMode?: boolean
+  } = {}) {
+    return new ComboboxMachine({
+      // @ts-expect-error TODO: Re-structure such that we don't need to ignore this
+      dataRef: { current: {} },
+      comboboxState: __demoMode ? ComboboxState.Open : ComboboxState.Closed,
+      isTyping: false,
+      options: [],
+      // @ts-expect-error TODO: Ensure we use the correct type
+      virtual: virtual
+        ? { options: virtual.options, disabled: virtual.disabled ?? (() => false) }
+        : null,
+      activeOptionIndex: null,
+      activationTrigger: ActivationTrigger.Other,
+      inputElement: null,
+      buttonElement: null,
+      optionsElement: null,
+      __demoMode,
+    })
+  }
+
+  actions = {
+    onChange: (newValue: T) => {
+      let { onChange, compare, mode, value } = this.state.dataRef.current
+
+      return match(mode, {
+        [ValueMode.Single]: () => {
+          return onChange?.(newValue)
+        },
+        [ValueMode.Multi]: () => {
+          let copy = (value as T[]).slice()
+
+          let idx = copy.findIndex((item) => compare(item, newValue))
+          if (idx === -1) {
+            copy.push(newValue)
+          } else {
+            copy.splice(idx, 1)
+          }
+
+          return onChange?.(copy as T)
+        },
+      })
+    },
+    registerOption: (id: string, dataRef: ComboboxOptionDataRef<T>) => {
+      this.send({ type: ActionTypes.RegisterOption, payload: { id, dataRef } })
+      return () => {
+        // When we are unregistering the currently active option, then we also have to make sure to
+        // reset the `defaultToFirstOption` flag, so that visually something is selected and the next
+        // time you press a key on your keyboard it will go to the proper next or previous option in
+        // the list.
+        //
+        // Since this was the active option and it could have been anywhere in the list, resetting to
+        // the very first option seems like a fine default. We _could_ be smarter about this by going
+        // to the previous / next item in list if we know the direction of the keyboard navigation,
+        // but that might be too complex/confusing from an end users perspective.
+        if (
+          this.state.activeOptionIndex ===
+          this.state.dataRef.current.calculateIndex(dataRef.current.value)
+        ) {
+          this.send({ type: ActionTypes.DefaultToFirstOption, value: true })
+        }
+
+        this.send({ type: ActionTypes.UnregisterOption, id })
+      }
+    },
+    goToOption: (
+      focus: { focus: Focus.Specific; idx: number } | { focus: Exclude<Focus, Focus.Specific> },
+      trigger?: ActivationTrigger
+    ) => {
+      this.send({ type: ActionTypes.DefaultToFirstOption, value: false })
+      return this.send({ type: ActionTypes.GoToOption, ...focus, trigger })
+    },
+    setIsTyping: (isTyping: boolean) => {
+      this.send({ type: ActionTypes.SetTyping, isTyping })
+    },
+    closeCombobox: () => {
+      this.send({ type: ActionTypes.CloseCombobox })
+      this.send({ type: ActionTypes.DefaultToFirstOption, value: false })
+      this.state.dataRef.current.onClose?.()
+    },
+    openCombobox: () => {
+      this.send({ type: ActionTypes.OpenCombobox })
+      this.send({ type: ActionTypes.DefaultToFirstOption, value: true })
+    },
+    setActivationTrigger: (trigger: ActivationTrigger) => {
+      this.send({ type: ActionTypes.SetActivationTrigger, trigger })
+    },
+    selectActiveOption: () => {
+      let activeOptionIndex = this.selectors.activeOptionIndex(this.state)
+      if (activeOptionIndex === null) return
+
+      this.actions.setIsTyping(false)
+
+      if (this.state.virtual) {
+        this.actions.onChange(this.state.virtual.options[activeOptionIndex])
+      } else {
+        let { dataRef } = this.state.options[activeOptionIndex]
+        this.actions.onChange(dataRef.current.value)
+      }
+
+      // It could happen that the `activeOptionIndex` stored in state is actually null, but we are
+      // getting the fallback active option back instead.
+      this.actions.goToOption({ focus: Focus.Specific, idx: activeOptionIndex })
+    },
+    setInputElement: (element: HTMLInputElement | null) => {
+      this.send({ type: ActionTypes.SetInputElement, element })
+    },
+    setButtonElement: (element: HTMLButtonElement | null) => {
+      this.send({ type: ActionTypes.SetButtonElement, element })
+    },
+    setOptionsElement: (element: HTMLElement | null) => {
+      this.send({ type: ActionTypes.SetOptionsElement, element })
+    },
+  }
+
+  selectors = {
+    activeDescendantId: (state: State<T>) => {
+      let activeOptionIndex = this.selectors.activeOptionIndex(state)
+      if (activeOptionIndex === null) {
+        return undefined
+      }
+
+      if (!state.virtual) {
+        return state.options[activeOptionIndex]?.id
+      }
+
+      return state.options.find((option) => {
+        return (
+          !option.dataRef.current.disabled &&
+          state.dataRef.current.compare(
+            option.dataRef.current.value,
+            state.virtual!.options[activeOptionIndex]
+          )
+        )
+      })?.id
+    },
+
+    activeOptionIndex: (state: State<T>) => {
+      if (
+        state.defaultToFirstOption &&
+        state.activeOptionIndex === null &&
+        (state.virtual ? state.virtual.options.length > 0 : state.options.length > 0)
+      ) {
+        if (state.virtual) {
+          let { options, disabled } = state.virtual
+          let activeOptionIndex = options.findIndex((option) => !(disabled?.(option) ?? false))
+
+          if (activeOptionIndex !== -1) {
+            return activeOptionIndex
+          }
+        }
+
+        let activeOptionIndex = state.options.findIndex((option) => {
+          return !option.dataRef.current.disabled
+        })
+
+        if (activeOptionIndex !== -1) {
+          return activeOptionIndex
+        }
+      }
+
+      return state.activeOptionIndex
+    },
+
+    activeOption: (state: State<T>) => {
+      let activeOptionIndex = this.selectors.activeOptionIndex(state)
+      return activeOptionIndex === null
+        ? null
+        : state.virtual
+          ? state.virtual.options[activeOptionIndex ?? 0]
+          : state.options[activeOptionIndex]?.dataRef.current.value ?? null
+    },
+
+    isActive: (state: State<T>, value: T, id: string) => {
+      let activeOptionIndex = this.selectors.activeOptionIndex(state)
+      if (activeOptionIndex === null) return false
+
+      if (state.virtual) {
+        return activeOptionIndex === state.dataRef.current.calculateIndex(value)
+      }
+
+      return state.options[activeOptionIndex]?.id === id
+    },
+
+    shouldScrollIntoView: (state: State<T>, value: T, id: string): boolean => {
+      if (state.virtual) return false
+      if (state.__demoMode) return false
+      if (state.comboboxState !== ComboboxState.Open) return false
+      if (state.activationTrigger === ActivationTrigger.Pointer) return false
+
+      let active = this.selectors.isActive(state, value, id)
+      if (!active) return false
+
+      return true
+    },
+  }
+
+  reduce(state: Readonly<State<T>>, action: Actions<T>): State<T> {
+    return match(action.type, reducers, state, action) as State<T>
+  }
+}
diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx
index fd364bc..f192364 100644
--- a/packages/@headlessui-react/src/components/combobox/combobox.tsx
+++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx
@@ -6,11 +6,9 @@ import { Virtualizer, useVirtualizer } from '@tanstack/react-virtual'
 import React, {
   Fragment,
   createContext,
-  createRef,
   useCallback,
   useContext,
   useMemo,
-  useReducer,
   useRef,
   useState,
   type CSSProperties,
@@ -57,12 +55,12 @@ import { FormFields } from '../../internal/form-fields'
 import { Frozen, useFrozenData } from '../../internal/frozen'
 import { useProvidedId } from '../../internal/id'
 import { OpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
+import { useSlice } from '../../react-glue'
 import type { EnsureArray, Props } from '../../types'
 import { history } from '../../utils/active-element-history'
 import { isDisabledReactIssue7711 } from '../../utils/bugs'
-import { Focus, calculateActiveIndex } from '../../utils/calculate-active-index'
+import { Focus } from '../../utils/calculate-active-index'
 import { disposables } from '../../utils/disposables'
-import { sortByDomNode } from '../../utils/focus-management'
 import { match } from '../../utils/match'
 import { isMobile } from '../../utils/platform'
 import {
@@ -79,398 +77,52 @@ import { Keys } from '../keyboard'
 import { Label, useLabelledBy, useLabels, type _internal_ComponentLabel } from '../label/label'
 import { MouseButton } from '../mouse'
 import { Portal } from '../portal/portal'
-
-enum ComboboxState {
-  Open,
-  Closed,
-}
-
-enum ValueMode {
-  Single,
-  Multi,
-}
-
-enum ActivationTrigger {
-  Pointer,
-  Focus,
-  Other,
-}
-
-type ComboboxOptionDataRef<T> = MutableRefObject<{
+import {
+  ActionTypes,
+  ActivationTrigger,
+  ComboboxState,
+  ValueMode,
+  type ComboboxOptionDataRef,
+} from './combobox-machine'
+import {
+  ComboboxContext,
+  useComboboxMachine,
+  useComboboxMachineContext,
+} from './combobox-machine-glue'
+
+let ComboboxDataContext = createContext<{
+  value: unknown
+  defaultValue: unknown
   disabled: boolean
-  value: T
-  domRef: MutableRefObject<HTMLElement | null>
-  order: number | null
-}>
-
-interface StateDefinition<T> {
-  dataRef: MutableRefObject<_Data | null>
-
-  virtual: { options: T[]; disabled: (value: unknown) => boolean } | null
-
-  comboboxState: ComboboxState
-
-  options: { id: string; dataRef: ComboboxOptionDataRef<T> }[]
-  activeOptionIndex: number | null
-  activationTrigger: ActivationTrigger
-
-  isTyping: boolean
+  invalid: boolean
+  mode: ValueMode
+  immediate: boolean
 
-  inputElement: HTMLInputElement | null
-  buttonElement: HTMLButtonElement | null
-  optionsElement: HTMLElement | null
+  virtual: { options: unknown[]; disabled: (value: unknown) => boolean } | null
+  calculateIndex(value: unknown): number
+  compare(a: unknown, z: unknown): boolean
+  isSelected(value: unknown): boolean
+  onChange(value: unknown): void
 
   __demoMode: boolean
-}
-
-enum ActionTypes {
-  OpenCombobox,
-  CloseCombobox,
-
-  GoToOption,
-  SetTyping,
-
-  RegisterOption,
-  UnregisterOption,
-
-  SetActivationTrigger,
 
-  UpdateVirtualConfiguration,
-
-  SetInputElement,
-  SetButtonElement,
-  SetOptionsElement,
-}
-
-function adjustOrderedState<T>(
-  state: StateDefinition<T>,
-  adjustment: (options: StateDefinition<T>['options']) => StateDefinition<T>['options'] = (i) => i
-) {
-  let currentActiveOption =
-    state.activeOptionIndex !== null ? state.options[state.activeOptionIndex] : null
-
-  let list = adjustment(state.options.slice())
-  let sortedOptions =
-    list.length > 0 && list[0].dataRef.current.order !== null
-      ? // Prefer sorting based on the `order`
-        list.sort((a, z) => a.dataRef.current.order! - z.dataRef.current.order!)
-      : // Fallback to much slower DOM order
-        sortByDomNode(list, (option) => option.dataRef.current.domRef.current)
-
-  // If we inserted an option before the current active option then the active option index
-  // would be wrong. To fix this, we will re-lookup the correct index.
-  let adjustedActiveOptionIndex = currentActiveOption
-    ? sortedOptions.indexOf(currentActiveOption)
-    : null
-
-  // Reset to `null` in case the currentActiveOption was removed.
-  if (adjustedActiveOptionIndex === -1) {
-    adjustedActiveOptionIndex = null
-  }
-
-  return {
-    options: sortedOptions,
-    activeOptionIndex: adjustedActiveOptionIndex,
-  }
-}
-
-type Actions<T> =
-  | { type: ActionTypes.CloseCombobox }
-  | { type: ActionTypes.OpenCombobox }
-  | {
-      type: ActionTypes.GoToOption
-      focus: Focus.Specific
-      idx: number
-      trigger?: ActivationTrigger
-    }
-  | { type: ActionTypes.SetTyping; isTyping: boolean }
-  | {
-      type: ActionTypes.GoToOption
-      focus: Exclude<Focus, Focus.Specific>
-      trigger?: ActivationTrigger
-    }
-  | {
-      type: ActionTypes.RegisterOption
-      payload: { id: string; dataRef: ComboboxOptionDataRef<T> }
-    }
-  | { type: ActionTypes.UnregisterOption; id: string }
-  | { type: ActionTypes.SetActivationTrigger; trigger: ActivationTrigger }
-  | {
-      type: ActionTypes.UpdateVirtualConfiguration
-      options: T[]
-      disabled: ((value: any) => boolean) | null
-    }
-  | { type: ActionTypes.SetInputElement; element: HTMLInputElement | null }
-  | { type: ActionTypes.SetButtonElement; element: HTMLButtonElement | null }
-  | { type: ActionTypes.SetOptionsElement; element: HTMLElement | null }
-
-let reducers: {
-  [P in ActionTypes]: <T>(
-    state: StateDefinition<T>,
-    action: Extract<Actions<T>, { type: P }>
-  ) => StateDefinition<T>
-} = {
-  [ActionTypes.CloseCombobox](state) {
-    if (state.dataRef.current?.disabled) return state
-    if (state.comboboxState === ComboboxState.Closed) return state
-
-    return {
-      ...state,
-      activeOptionIndex: null,
-      comboboxState: ComboboxState.Closed,
-
-      isTyping: false,
-
-      // Clear the last known activation trigger
-      // This is because if a user interacts with the combobox using a mouse
-      // resulting in it closing we might incorrectly handle the next interaction
-      // for example, not scrolling to the active option in a virtual list
-      activationTrigger: ActivationTrigger.Other,
-
-      __demoMode: false,
-    }
-  },
-  [ActionTypes.OpenCombobox](state) {
-    if (state.dataRef.current?.disabled) return state
-    if (state.comboboxState === ComboboxState.Open) return state
-
-    // Check if we have a selected value that we can make active
-    if (state.dataRef.current?.value) {
-      let idx = state.dataRef.current.calculateIndex(state.dataRef.current.value)
-      if (idx !== -1) {
-        return {
-          ...state,
-          activeOptionIndex: idx,
-          comboboxState: ComboboxState.Open,
-          __demoMode: false,
-        }
-      }
-    }
-
-    return { ...state, comboboxState: ComboboxState.Open, __demoMode: false }
-  },
-  [ActionTypes.SetTyping](state, action) {
-    if (state.isTyping === action.isTyping) return state
-    return { ...state, isTyping: action.isTyping }
-  },
-  [ActionTypes.GoToOption](state, action) {
-    if (state.dataRef.current?.disabled) return state
-    if (
-      state.optionsElement &&
-      !state.dataRef.current?.optionsPropsRef.current.static &&
-      state.comboboxState === ComboboxState.Closed
-    ) {
-      return state
-    }
-
-    if (state.virtual) {
-      let { options, disabled } = state.virtual
-      let activeOptionIndex =
-        action.focus === Focus.Specific
-          ? action.idx
-          : calculateActiveIndex(action, {
-              resolveItems: () => options,
-              resolveActiveIndex: () =>
-                state.activeOptionIndex ?? options.findIndex((option) => !disabled(option)) ?? null,
-              resolveDisabled: disabled,
-              resolveId() {
-                throw new Error('Function not implemented.')
-              },
-            })
-
-      let activationTrigger = action.trigger ?? ActivationTrigger.Other
-
-      if (
-        state.activeOptionIndex === activeOptionIndex &&
-        state.activationTrigger === activationTrigger
-      ) {
-        return state
-      }
-
-      return {
-        ...state,
-        activeOptionIndex,
-        activationTrigger,
-        isTyping: false,
-        __demoMode: false,
-      }
-    }
-
-    let adjustedState = adjustOrderedState(state)
-
-    // It's possible that the activeOptionIndex is set to `null` internally, but
-    // this means that we will fallback to the first non-disabled option by default.
-    // We have to take this into account.
-    if (adjustedState.activeOptionIndex === null) {
-      let localActiveOptionIndex = adjustedState.options.findIndex(
-        (option) => !option.dataRef.current.disabled
-      )
-
-      if (localActiveOptionIndex !== -1) {
-        adjustedState.activeOptionIndex = localActiveOptionIndex
-      }
-    }
-
-    let activeOptionIndex =
-      action.focus === Focus.Specific
-        ? action.idx
-        : calculateActiveIndex(action, {
-            resolveItems: () => adjustedState.options,
-            resolveActiveIndex: () => adjustedState.activeOptionIndex,
-            resolveId: (item) => item.id,
-            resolveDisabled: (item) => item.dataRef.current.disabled,
-          })
-    let activationTrigger = action.trigger ?? ActivationTrigger.Other
-
-    if (
-      state.activeOptionIndex === activeOptionIndex &&
-      state.activationTrigger === activationTrigger
-    ) {
-      return state
-    }
-
-    return {
-      ...state,
-      ...adjustedState,
-      isTyping: false,
-      activeOptionIndex,
-      activationTrigger,
-      __demoMode: false,
-    }
-  },
-  [ActionTypes.RegisterOption]: (state, action) => {
-    if (state.dataRef.current?.virtual) {
-      return {
-        ...state,
-        options: [...state.options, action.payload],
-      }
-    }
-
-    let option = action.payload
-
-    let adjustedState = adjustOrderedState(state, (options) => {
-      options.push(option)
-      return options
-    })
-
-    // Check if we need to make the newly registered option active.
-    if (state.activeOptionIndex === null) {
-      if (state.dataRef.current?.isSelected(action.payload.dataRef.current.value)) {
-        adjustedState.activeOptionIndex = adjustedState.options.indexOf(option)
-      }
-    }
-
-    let nextState = {
-      ...state,
-      ...adjustedState,
-      activationTrigger: ActivationTrigger.Other,
-    }
-
-    if (state.dataRef.current?.__demoMode && state.dataRef.current.value === undefined) {
-      nextState.activeOptionIndex = 0
-    }
-
-    return nextState
-  },
-  [ActionTypes.UnregisterOption]: (state, action) => {
-    if (state.dataRef.current?.virtual) {
-      return {
-        ...state,
-        options: state.options.filter((option) => option.id !== action.id),
-      }
-    }
-
-    let adjustedState = adjustOrderedState(state, (options) => {
-      let idx = options.findIndex((option) => option.id === action.id)
-      if (idx !== -1) options.splice(idx, 1)
-      return options
-    })
-
-    return {
-      ...state,
-      ...adjustedState,
-      activationTrigger: ActivationTrigger.Other,
-    }
-  },
-  [ActionTypes.SetActivationTrigger]: (state, action) => {
-    if (state.activationTrigger === action.trigger) {
-      return state
-    }
-
-    return {
-      ...state,
-      activationTrigger: action.trigger,
-    }
-  },
-  [ActionTypes.UpdateVirtualConfiguration]: (state, action) => {
-    if (state.virtual === null) {
-      return {
-        ...state,
-        virtual: { options: action.options, disabled: action.disabled ?? (() => false) },
-      }
-    }
-
-    if (state.virtual.options === action.options && state.virtual.disabled === action.disabled) {
-      return state
-    }
-
-    let adjustedActiveOptionIndex = state.activeOptionIndex
-    if (state.activeOptionIndex !== null) {
-      let idx = action.options.indexOf(state.virtual.options[state.activeOptionIndex])
-      if (idx !== -1) {
-        adjustedActiveOptionIndex = idx
-      } else {
-        adjustedActiveOptionIndex = null
-      }
-    }
-
-    return {
-      ...state,
-      activeOptionIndex: adjustedActiveOptionIndex,
-      virtual: { options: action.options, disabled: action.disabled ?? (() => false) },
-    }
-  },
-  [ActionTypes.SetInputElement]: (state, action) => {
-    if (state.inputElement === action.element) return state
-    return { ...state, inputElement: action.element }
-  },
-  [ActionTypes.SetButtonElement]: (state, action) => {
-    if (state.buttonElement === action.element) return state
-    return { ...state, buttonElement: action.element }
-  },
-  [ActionTypes.SetOptionsElement]: (state, action) => {
-    if (state.optionsElement === action.element) return state
-    return { ...state, optionsElement: action.element }
-  },
-}
-
-let ComboboxActionsContext = createContext<{
-  openCombobox(): void
-  closeCombobox(): void
-  registerOption(id: string, dataRef: ComboboxOptionDataRef<unknown>): () => void
-  goToOption(focus: Focus.Specific, idx: number, trigger?: ActivationTrigger): void
-  goToOption(focus: Focus, idx?: number, trigger?: ActivationTrigger): void
-  setIsTyping(isTyping: boolean): void
-  selectActiveOption(): void
-  setActivationTrigger(trigger: ActivationTrigger): void
-  onChange(value: unknown): void
-
-  setInputElement(element: HTMLInputElement | null): void
-  setButtonElement(element: HTMLButtonElement | null): void
-  setOptionsElement(element: HTMLElement | null): void
+  optionsPropsRef: MutableRefObject<{
+    static: boolean
+    hold: boolean
+  }>
 } | null>(null)
-ComboboxActionsContext.displayName = 'ComboboxActionsContext'
+ComboboxDataContext.displayName = 'ComboboxDataContext'
 
-function useActions(component: string) {
-  let context = useContext(ComboboxActionsContext)
+function useData(component: string) {
+  let context = useContext(ComboboxDataContext)
   if (context === null) {
     let err = new Error(`<${component} /> is missing a parent <Combobox /> component.`)
-    if (Error.captureStackTrace) Error.captureStackTrace(err, useActions)
+    if (Error.captureStackTrace) Error.captureStackTrace(err, useData)
     throw err
   }
   return context
 }
-type _Actions = ReturnType<typeof useActions>
+type _Data = ReturnType<typeof useData>
 
 let VirtualContext = createContext<Virtualizer<any, any> | null>(null)
 
@@ -478,12 +130,14 @@ function VirtualProvider(props: {
   slot: OptionsRenderPropArg
   children: (data: { option: unknown; open: boolean }) => React.ReactElement
 }) {
+  let machine = useComboboxMachineContext('VirtualProvider')
   let data = useData('VirtualProvider')
-  let d = useDisposables()
   let { options } = data.virtual!
 
+  let optionsElement = useSlice(machine, (state) => state.optionsElement)
+
   let [paddingStart, paddingEnd] = useMemo(() => {
-    let el = data.optionsElement
+    let el = optionsElement
     if (!el) return [0, 0]
 
     let styles = window.getComputedStyle(el)
@@ -492,7 +146,7 @@ function VirtualProvider(props: {
       parseFloat(styles.paddingBlockStart || styles.paddingTop),
       parseFloat(styles.paddingBlockEnd || styles.paddingBottom),
     ]
-  }, [data.optionsElement])
+  }, [optionsElement])
 
   let virtualizer = useVirtualizer({
     enabled: options.length !== 0,
@@ -503,7 +157,7 @@ function VirtualProvider(props: {
       return 40
     },
     getScrollElement() {
-      return data.optionsElement
+      return machine.state.optionsElement
     },
     overscan: 12,
   })
@@ -515,6 +169,11 @@ function VirtualProvider(props: {
 
   let items = virtualizer.getVirtualItems()
 
+  let isPointerActivationTrigger = useSlice(machine, (state) => {
+    return state.activationTrigger === ActivationTrigger.Pointer
+  })
+  let activeOptionIndex = useSlice(machine, machine.selectors.activeOptionIndex)
+
   if (items.length === 0) {
     return null
   }
@@ -528,24 +187,15 @@ function VirtualProvider(props: {
           height: `${virtualizer.getTotalSize()}px`,
         }}
         ref={(el) => {
-          if (!el) {
-            d.dispose()
-            return
-          }
+          if (!el) return
 
           // Do not scroll when the mouse/pointer is being used
-          if (data.activationTrigger === ActivationTrigger.Pointer) {
-            return
-          }
+          if (isPointerActivationTrigger) return
 
           // Scroll to the active index
-          //
-          // Workaround for: https://github.com/TanStack/virtual/issues/879
-          d.nextFrame(() => {
-            if (data.activeOptionIndex !== null && options.length > data.activeOptionIndex) {
-              virtualizer.scrollToIndex(data.activeOptionIndex)
-            }
-          })
+          if (activeOptionIndex !== null && options.length > activeOptionIndex) {
+            virtualizer.scrollToIndex(activeOptionIndex)
+          }
         }}
       >
         {items.map((item) => {
@@ -578,48 +228,6 @@ function VirtualProvider(props: {
   )
 }
 
-let ComboboxDataContext = createContext<
-  | ({
-      value: unknown
-      defaultValue: unknown
-      disabled: boolean
-      invalid: boolean
-      mode: ValueMode
-      activeOptionIndex: number | null
-      immediate: boolean
-
-      virtual: { options: unknown[]; disabled: (value: unknown) => boolean } | null
-      calculateIndex(value: unknown): number
-      compare(a: unknown, z: unknown): boolean
-      isSelected(value: unknown): boolean
-      isActive(value: unknown): boolean
-
-      __demoMode: boolean
-
-      optionsPropsRef: MutableRefObject<{
-        static: boolean
-        hold: boolean
-      }>
-    } & Omit<StateDefinition<unknown>, 'dataRef'>)
-  | null
->(null)
-ComboboxDataContext.displayName = 'ComboboxDataContext'
-
-function useData(component: string) {
-  let context = useContext(ComboboxDataContext)
-  if (context === null) {
-    let err = new Error(`<${component} /> is missing a parent <Combobox /> component.`)
-    if (Error.captureStackTrace) Error.captureStackTrace(err, useData)
-    throw err
-  }
-  return context
-}
-type _Data = ReturnType<typeof useData>
-
-function stateReducer<T>(state: StateDefinition<T>, action: Actions<T>) {
-  return match(action.type, reducers, state, action)
-}
-
 // ---
 
 let DEFAULT_COMBOBOX_TAG = Fragment
@@ -687,7 +295,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
     by,
     invalid = false,
     disabled = providedDisabled || false,
-    onClose,
+    onClose: theirOnClose,
     __demoMode = false,
     multiple = false,
     immediate = false,
@@ -704,23 +312,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
     defaultValue
   )
 
-  let [state, dispatch] = useReducer(stateReducer, {
-    dataRef: createRef(),
-    comboboxState: __demoMode ? ComboboxState.Open : ComboboxState.Closed,
-    isTyping: false,
-    options: [],
-    virtual: virtual
-      ? { options: virtual.options, disabled: virtual.disabled ?? (() => false) }
-      : null,
-    activeOptionIndex: null,
-    activationTrigger: ActivationTrigger.Other,
-    inputElement: null,
-    buttonElement: null,
-    optionsElement: null,
-    __demoMode,
-  } as StateDefinition<TValue>)
-
-  let defaultToFirstOption = useRef(false)
+  let machine = useComboboxMachine({ virtual, __demoMode })
 
   let optionsPropsRef = useRef<_Data['optionsPropsRef']['current']>({ static: false, hold: false })
 
@@ -735,27 +327,27 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
         return virtual.options.findIndex((other) => compare(other, value))
       }
     } else {
-      return state.options.findIndex((other) => compare(other.dataRef.current.value, value))
+      return machine.state.options.findIndex((other) => compare(other.dataRef.current.value, value))
     }
   })
 
   let isSelected: (value: TValue) => boolean = useCallback(
-    (other) =>
-      match(data.mode, {
-        [ValueMode.Multi]: () =>
-          (value as EnsureArray<TValue>).some((option) => compare(option, other)),
+    (other) => {
+      return match(data.mode, {
+        [ValueMode.Multi]: () => {
+          return (value as EnsureArray<TValue>).some((option) => compare(option, other))
+        },
         [ValueMode.Single]: () => compare(value as TValue, other),
-      }),
+      })
+    },
     [value]
   )
 
-  let isActive = useEvent((other: TValue) => {
-    return state.activeOptionIndex === calculateIndex(other)
-  })
-
+  let virtualSlice = useSlice(machine, (state) => state.virtual)
+  let onClose = useEvent(() => theirOnClose?.())
   let data = useMemo<_Data>(
     () => ({
-      ...state,
+      __demoMode,
       immediate,
       optionsPropsRef,
       value,
@@ -763,45 +355,32 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
       disabled,
       invalid,
       mode: multiple ? ValueMode.Multi : ValueMode.Single,
-      virtual: virtual ? state.virtual : null,
-      get activeOptionIndex() {
-        if (
-          defaultToFirstOption.current &&
-          state.activeOptionIndex === null &&
-          (virtual ? virtual.options.length > 0 : state.options.length > 0)
-        ) {
-          if (virtual) {
-            let localActiveOptionIndex = virtual.options.findIndex(
-              (option) => !(virtual.disabled?.(option) ?? false)
-            )
-
-            if (localActiveOptionIndex !== -1) {
-              return localActiveOptionIndex
-            }
-          }
-
-          let localActiveOptionIndex = state.options.findIndex((option) => {
-            return !option.dataRef.current.disabled
-          })
-
-          if (localActiveOptionIndex !== -1) {
-            return localActiveOptionIndex
-          }
-        }
-
-        return state.activeOptionIndex
-      },
+      virtual: virtual ? virtualSlice : null,
+      onChange: theirOnChange,
+      isSelected,
       calculateIndex,
       compare,
-      isSelected,
-      isActive,
+      onClose,
     }),
-    [value, defaultValue, disabled, invalid, multiple, __demoMode, state, virtual]
+    [
+      value,
+      defaultValue,
+      disabled,
+      invalid,
+      multiple,
+      theirOnChange,
+      isSelected,
+      __demoMode,
+      machine,
+      virtual,
+      virtualSlice,
+      onClose,
+    ]
   )
 
   useIsoMorphicEffect(() => {
     if (!virtual) return
-    dispatch({
+    machine.send({
       type: ActionTypes.UpdateVirtualConfiguration,
       options: virtual.options,
       disabled: virtual.disabled ?? null,
@@ -809,147 +388,35 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
   }, [virtual, virtual?.options, virtual?.disabled])
 
   useIsoMorphicEffect(() => {
-    state.dataRef.current = data
+    machine.state.dataRef.current = data
   }, [data])
 
+  let [comboboxState, buttonElement, inputElement, optionsElement] = useSlice(machine, (state) => [
+    state.comboboxState,
+    state.buttonElement,
+    state.inputElement,
+    state.optionsElement,
+  ])
+
   // Handle outside click
-  let outsideClickEnabled = data.comboboxState === ComboboxState.Open
-  useOutsideClick(
-    outsideClickEnabled,
-    [data.buttonElement, data.inputElement, data.optionsElement],
-    () => actions.closeCombobox()
+  let outsideClickEnabled = comboboxState === ComboboxState.Open
+  useOutsideClick(outsideClickEnabled, [buttonElement, inputElement, optionsElement], () =>
+    machine.actions.closeCombobox()
   )
 
+  let activeOptionIndex = useSlice(machine, machine.selectors.activeOptionIndex)
+  let activeOption = useSlice(machine, machine.selectors.activeOption)
+
   let slot = useMemo(() => {
     return {
-      open: data.comboboxState === ComboboxState.Open,
+      open: comboboxState === ComboboxState.Open,
       disabled,
       invalid,
-      activeIndex: data.activeOptionIndex,
-      activeOption:
-        data.activeOptionIndex === null
-          ? null
-          : data.virtual
-            ? data.virtual.options[data.activeOptionIndex ?? 0]
-            : (data.options[data.activeOptionIndex]?.dataRef.current.value as TValue) ?? null,
+      activeIndex: activeOptionIndex,
+      activeOption,
       value,
     } satisfies ComboboxRenderPropArg<unknown>
-  }, [data, disabled, value, invalid])
-
-  let selectActiveOption = useEvent(() => {
-    if (data.activeOptionIndex === null) return
-
-    actions.setIsTyping(false)
-
-    if (data.virtual) {
-      onChange(data.virtual.options[data.activeOptionIndex])
-    } else {
-      let { dataRef } = data.options[data.activeOptionIndex]
-      onChange(dataRef.current.value)
-    }
-
-    // It could happen that the `activeOptionIndex` stored in state is actually null, but we are
-    // getting the fallback active option back instead.
-    actions.goToOption(Focus.Specific, data.activeOptionIndex)
-  })
-
-  let openCombobox = useEvent(() => {
-    dispatch({ type: ActionTypes.OpenCombobox })
-    defaultToFirstOption.current = true
-  })
-
-  let closeCombobox = useEvent(() => {
-    dispatch({ type: ActionTypes.CloseCombobox })
-    defaultToFirstOption.current = false
-    onClose?.()
-  })
-
-  let setIsTyping = useEvent((isTyping: boolean) => {
-    dispatch({ type: ActionTypes.SetTyping, isTyping })
-  })
-
-  let goToOption = useEvent((focus, idx, trigger) => {
-    defaultToFirstOption.current = false
-
-    if (focus === Focus.Specific) {
-      return dispatch({ type: ActionTypes.GoToOption, focus: Focus.Specific, idx: idx!, trigger })
-    }
-
-    return dispatch({ type: ActionTypes.GoToOption, focus, trigger })
-  })
-
-  let registerOption = useEvent((id, dataRef) => {
-    dispatch({ type: ActionTypes.RegisterOption, payload: { id, dataRef } })
-    return () => {
-      // When we are unregistering the currently active option, then we also have to make sure to
-      // reset the `defaultToFirstOption` flag, so that visually something is selected and the next
-      // time you press a key on your keyboard it will go to the proper next or previous option in
-      // the list.
-      //
-      // Since this was the active option and it could have been anywhere in the list, resetting to
-      // the very first option seems like a fine default. We _could_ be smarter about this by going
-      // to the previous / next item in list if we know the direction of the keyboard navigation,
-      // but that might be too complex/confusing from an end users perspective.
-      if (data.isActive(dataRef.current.value)) {
-        defaultToFirstOption.current = true
-      }
-
-      dispatch({ type: ActionTypes.UnregisterOption, id })
-    }
-  })
-
-  let onChange = useEvent((value: unknown) => {
-    return match(data.mode, {
-      [ValueMode.Single]() {
-        return theirOnChange?.(value as TValue)
-      },
-      [ValueMode.Multi]() {
-        let copy = (data.value as TValue[]).slice()
-
-        let idx = copy.findIndex((item) => compare(item, value as TValue))
-        if (idx === -1) {
-          copy.push(value as TValue)
-        } else {
-          copy.splice(idx, 1)
-        }
-
-        return theirOnChange?.(copy as TValue[])
-      },
-    })
-  })
-
-  let setActivationTrigger = useEvent((trigger: ActivationTrigger) => {
-    dispatch({ type: ActionTypes.SetActivationTrigger, trigger })
-  })
-
-  let setInputElement = useEvent((element: HTMLInputElement | null) => {
-    dispatch({ type: ActionTypes.SetInputElement, element })
-  })
-
-  let setButtonElement = useEvent((element: HTMLButtonElement | null) => {
-    dispatch({ type: ActionTypes.SetButtonElement, element })
-  })
-
-  let setOptionsElement = useEvent((element: HTMLElement | null) => {
-    dispatch({ type: ActionTypes.SetOptionsElement, element })
-  })
-
-  let actions = useMemo<_Actions>(
-    () => ({
-      onChange,
-      registerOption,
-      goToOption,
-      setIsTyping,
-      closeCombobox,
-      openCombobox,
-      setActivationTrigger,
-      selectActiveOption,
-      setInputElement,
-      setButtonElement,
-      setOptionsElement,
-    }),
-    []
-  )
+  }, [data, disabled, value, invalid, activeOption, comboboxState])
 
   let [labelledby, LabelProvider] = useLabels()
 
@@ -966,18 +433,18 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
     <LabelProvider
       value={labelledby}
       props={{
-        htmlFor: data.inputElement?.id,
+        htmlFor: inputElement?.id,
       }}
       slot={{
-        open: data.comboboxState === ComboboxState.Open,
+        open: comboboxState === ComboboxState.Open,
         disabled,
       }}
     >
       <FloatingProvider>
-        <ComboboxActionsContext.Provider value={actions}>
-          <ComboboxDataContext.Provider value={data}>
+        <ComboboxDataContext.Provider value={data}>
+          <ComboboxContext.Provider value={machine}>
             <OpenClosedProvider
-              value={match(data.comboboxState, {
+              value={match(comboboxState, {
                 [ComboboxState.Open]: State.Open,
                 [ComboboxState.Closed]: State.Closed,
               })}
@@ -998,8 +465,8 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
                 name: 'Combobox',
               })}
             </OpenClosedProvider>
-          </ComboboxDataContext.Provider>
-        </ComboboxActionsContext.Provider>
+          </ComboboxContext.Provider>
+        </ComboboxDataContext.Provider>
       </FloatingProvider>
     </LabelProvider>
   )
@@ -1047,8 +514,8 @@ function InputFn<
   // But today is not that day..
   TType = Parameters<typeof ComboboxRoot>[0]['value'],
 >(props: ComboboxInputProps<TTag, TType>, ref: Ref<HTMLInputElement>) {
+  let machine = useComboboxMachineContext('Combobox.Input')
   let data = useData('Combobox.Input')
-  let actions = useActions('Combobox.Input')
 
   let internalId = useId()
   let providedId = useProvidedId()
@@ -1063,18 +530,30 @@ function InputFn<
     ...theirProps
   } = props
 
+  let [inputElement] = useSlice(machine, (state) => [state.inputElement])
+
   let internalInputRef = useRef<HTMLInputElement | null>(null)
-  let inputRef = useSyncRefs(internalInputRef, ref, useFloatingReference(), actions.setInputElement)
-  let ownerDocument = useOwnerDocument(data.inputElement)
+  let inputRef = useSyncRefs(
+    internalInputRef,
+    ref,
+    useFloatingReference(),
+    machine.actions.setInputElement
+  )
+  let ownerDocument = useOwnerDocument(inputElement)
+
+  let [comboboxState, isTyping] = useSlice(machine, (state) => [
+    state.comboboxState,
+    state.isTyping,
+  ])
 
   let d = useDisposables()
 
   let clear = useEvent(() => {
-    actions.onChange(null)
-    if (data.optionsElement) {
-      data.optionsElement.scrollTop = 0
+    machine.actions.onChange(null)
+    if (machine.state.optionsElement) {
+      machine.state.optionsElement.scrollTop = 0
     }
-    actions.goToOption(Focus.Nothing)
+    machine.actions.goToOption({ focus: Focus.Nothing })
   })
 
   // When a `displayValue` prop is given, we should use it to transform the current selected
@@ -1114,7 +593,7 @@ function InputFn<
     ([currentDisplayValue, state], [oldCurrentDisplayValue, oldState]) => {
       // When the user is typing, we want to not touch the `input` at all. Especially when they are
       // using an IME, we don't want to mess with the input at all.
-      if (data.isTyping) return
+      if (machine.state.isTyping) return
 
       let input = internalInputRef.current
       if (!input) return
@@ -1130,7 +609,7 @@ function InputFn<
       // the user is currently typing, because we don't want to mess with the cursor position while
       // typing.
       requestAnimationFrame(() => {
-        if (data.isTyping) return
+        if (machine.state.isTyping) return
         if (!input) return
 
         // Bail when the input is not the currently focused element. When it is not the focused
@@ -1150,7 +629,7 @@ function InputFn<
         input.setSelectionRange(input.value.length, input.value.length)
       })
     },
-    [currentDisplayValue, data.comboboxState, ownerDocument, data.isTyping]
+    [currentDisplayValue, comboboxState, ownerDocument, isTyping]
   )
 
   // Trick VoiceOver in behaving a little bit better. Manually "resetting" the input makes VoiceOver
@@ -1164,7 +643,7 @@ function InputFn<
       if (newState === ComboboxState.Open && oldState === ComboboxState.Closed) {
         // When the user is typing, we want to not touch the `input` at all. Especially when they are
         // using an IME, we don't want to mess with the input at all.
-        if (data.isTyping) return
+        if (machine.state.isTyping) return
 
         let input = internalInputRef.current
         if (!input) return
@@ -1185,7 +664,7 @@ function InputFn<
         }
       }
     },
-    [data.comboboxState]
+    [comboboxState]
   )
 
   let isComposing = useRef(false)
@@ -1199,13 +678,13 @@ function InputFn<
   })
 
   let handleKeyDown = useEvent((event: ReactKeyboardEvent<HTMLInputElement>) => {
-    actions.setIsTyping(true)
+    machine.actions.setIsTyping(true)
 
     switch (event.key) {
       // Ref: https://www.w3.org/WAI/ARIA/apg/patterns/menu/#keyboard-interaction-12
 
       case Keys.Enter:
-        if (data.comboboxState !== ComboboxState.Open) return
+        if (machine.state.comboboxState !== ComboboxState.Open) return
 
         // When the user is still in the middle of composing by using an IME, then we don't want to
         // submit this value and close the Combobox yet. Instead, we will fallback to the default
@@ -1215,14 +694,14 @@ function InputFn<
         event.preventDefault()
         event.stopPropagation()
 
-        if (data.activeOptionIndex === null) {
-          actions.closeCombobox()
+        if (machine.selectors.activeOptionIndex(machine.state) === null) {
+          machine.actions.closeCombobox()
           return
         }
 
-        actions.selectActiveOption()
+        machine.actions.selectActiveOption()
         if (data.mode === ValueMode.Single) {
-          actions.closeCombobox()
+          machine.actions.closeCombobox()
         }
         break
 
@@ -1230,19 +709,19 @@ function InputFn<
         event.preventDefault()
         event.stopPropagation()
 
-        return match(data.comboboxState, {
-          [ComboboxState.Open]: () => actions.goToOption(Focus.Next),
-          [ComboboxState.Closed]: () => actions.openCombobox(),
+        return match(machine.state.comboboxState, {
+          [ComboboxState.Open]: () => machine.actions.goToOption({ focus: Focus.Next }),
+          [ComboboxState.Closed]: () => machine.actions.openCombobox(),
         })
 
       case Keys.ArrowUp:
         event.preventDefault()
         event.stopPropagation()
-        return match(data.comboboxState, {
-          [ComboboxState.Open]: () => actions.goToOption(Focus.Previous),
+        return match(machine.state.comboboxState, {
+          [ComboboxState.Open]: () => machine.actions.goToOption({ focus: Focus.Previous }),
           [ComboboxState.Closed]: () => {
-            flushSync(() => actions.openCombobox())
-            if (!data.value) actions.goToOption(Focus.Last)
+            flushSync(() => machine.actions.openCombobox())
+            if (!data.value) machine.actions.goToOption({ focus: Focus.Last })
           },
         })
 
@@ -1253,12 +732,12 @@ function InputFn<
 
         event.preventDefault()
         event.stopPropagation()
-        return actions.goToOption(Focus.First)
+        return machine.actions.goToOption({ focus: Focus.First })
 
       case Keys.PageUp:
         event.preventDefault()
         event.stopPropagation()
-        return actions.goToOption(Focus.First)
+        return machine.actions.goToOption({ focus: Focus.First })
 
       case Keys.End:
         if (event.shiftKey) {
@@ -1267,17 +746,17 @@ function InputFn<
 
         event.preventDefault()
         event.stopPropagation()
-        return actions.goToOption(Focus.Last)
+        return machine.actions.goToOption({ focus: Focus.Last })
 
       case Keys.PageDown:
         event.preventDefault()
         event.stopPropagation()
-        return actions.goToOption(Focus.Last)
+        return machine.actions.goToOption({ focus: Focus.Last })
 
       case Keys.Escape:
-        if (data.comboboxState !== ComboboxState.Open) return
+        if (machine.state.comboboxState !== ComboboxState.Open) return
         event.preventDefault()
-        if (data.optionsElement && !data.optionsPropsRef.current.static) {
+        if (machine.state.optionsElement && !data.optionsPropsRef.current.static) {
           event.stopPropagation()
         }
 
@@ -1292,14 +771,17 @@ function InputFn<
           }
         }
 
-        return actions.closeCombobox()
+        return machine.actions.closeCombobox()
 
       case Keys.Tab:
-        if (data.comboboxState !== ComboboxState.Open) return
-        if (data.mode === ValueMode.Single && data.activationTrigger !== ActivationTrigger.Focus) {
-          actions.selectActiveOption()
+        if (machine.state.comboboxState !== ComboboxState.Open) return
+        if (
+          data.mode === ValueMode.Single &&
+          machine.state.activationTrigger !== ActivationTrigger.Focus
+        ) {
+          machine.actions.selectActiveOption()
         }
-        actions.closeCombobox()
+        machine.actions.closeCombobox()
         break
     }
   })
@@ -1323,7 +805,7 @@ function InputFn<
     }
 
     // Open the combobox to show the results based on what the user has typed
-    actions.openCombobox()
+    machine.actions.openCombobox()
   })
 
   let handleBlur = useEvent((event: ReactFocusEvent) => {
@@ -1331,17 +813,17 @@ function InputFn<
       (event.relatedTarget as HTMLElement) ?? history.find((x) => x !== event.currentTarget)
 
     // Focus is moved into the list, we don't want to close yet.
-    if (data.optionsElement?.contains(relatedTarget)) return
+    if (machine.state.optionsElement?.contains(relatedTarget)) return
 
     // Focus is moved to the button, we don't want to close yet.
-    if (data.buttonElement?.contains(relatedTarget)) return
+    if (machine.state.buttonElement?.contains(relatedTarget)) return
 
     // Focus is moved, but the combobox is not open. This can mean two things:
     //
     // 1. The combobox was never opened, so we don't have to do anything.
     // 2. The combobox was closed and focus was moved already. At that point we
     //    don't need to try and select the active option.
-    if (data.comboboxState !== ComboboxState.Open) return
+    if (machine.state.comboboxState !== ComboboxState.Open) return
 
     event.preventDefault()
 
@@ -1355,18 +837,18 @@ function InputFn<
       clear()
     }
 
-    return actions.closeCombobox()
+    return machine.actions.closeCombobox()
   })
 
   let handleFocus = useEvent((event: ReactFocusEvent) => {
     let relatedTarget =
       (event.relatedTarget as HTMLElement) ?? history.find((x) => x !== event.currentTarget)
-    if (data.buttonElement?.contains(relatedTarget)) return
-    if (data.optionsElement?.contains(relatedTarget)) return
+    if (machine.state.buttonElement?.contains(relatedTarget)) return
+    if (machine.state.optionsElement?.contains(relatedTarget)) return
     if (data.disabled) return
 
     if (!data.immediate) return
-    if (data.comboboxState === ComboboxState.Open) return
+    if (machine.state.comboboxState === ComboboxState.Open) return
 
     // In a scenario where you have this setup:
     //
@@ -1391,13 +873,13 @@ function InputFn<
     // Which is why we wrap this in a `microTask` to make sure we are not in the
     // middle of rendering.
     d.microTask(() => {
-      flushSync(() => actions.openCombobox())
+      flushSync(() => machine.actions.openCombobox())
 
       // We need to make sure that tabbing through a form doesn't result in
       // incorrectly setting the value of the combobox. We will set the
       // activation trigger to `Focus`, and we will ignore selecting the active
       // option when the user tabs away.
-      actions.setActivationTrigger(ActivationTrigger.Focus)
+      machine.actions.setActivationTrigger(ActivationTrigger.Focus)
     })
   })
 
@@ -1407,9 +889,11 @@ function InputFn<
   let { isFocused: focus, focusProps } = useFocusRing({ autoFocus })
   let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
 
+  let optionsElement = useSlice(machine, (state) => state.optionsElement)
+
   let slot = useMemo(() => {
     return {
-      open: data.comboboxState === ComboboxState.Open,
+      open: comboboxState === ComboboxState.Open,
       disabled,
       invalid: data.invalid,
       hover,
@@ -1424,21 +908,9 @@ function InputFn<
       id,
       role: 'combobox',
       type,
-      'aria-controls': data.optionsElement?.id,
-      'aria-expanded': data.comboboxState === ComboboxState.Open,
-      'aria-activedescendant':
-        data.activeOptionIndex === null
-          ? undefined
-          : data.virtual
-            ? data.options.find(
-                (option) =>
-                  !option.dataRef.current.disabled &&
-                  data.compare(
-                    option.dataRef.current.value,
-                    data.virtual!.options[data.activeOptionIndex!]
-                  )
-              )?.id
-            : data.options[data.activeOptionIndex]?.id,
+      'aria-controls': optionsElement?.id,
+      'aria-expanded': comboboxState === ComboboxState.Open,
+      'aria-activedescendant': useSlice(machine, machine.selectors.activeDescendantId),
       'aria-labelledby': labelledBy,
       'aria-describedby': describedBy,
       'aria-autocomplete': 'list',
@@ -1504,9 +976,10 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
   props: ComboboxButtonProps<TTag>,
   ref: Ref<HTMLButtonElement>
 ) {
+  let machine = useComboboxMachineContext('Combobox.Button')
   let data = useData('Combobox.Button')
-  let actions = useActions('Combobox.Button')
-  let buttonRef = useSyncRefs(ref, actions.setButtonElement)
+  let [localButtonElement, setLocalButtonElement] = useState<HTMLButtonElement | null>(null)
+  let buttonRef = useSyncRefs(ref, setLocalButtonElement, machine.actions.setButtonElement)
 
   let internalId = useId()
   let {
@@ -1516,7 +989,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
     ...theirProps
   } = props
 
-  let refocusInput = useRefocusableInput(data.inputElement)
+  let inputElement = useSlice(machine, (state) => state.inputElement)
+  let refocusInput = useRefocusableInput(inputElement)
 
   let handleKeyDown = useEvent((event: ReactKeyboardEvent<HTMLElement>) => {
     switch (event.key) {
@@ -1526,8 +1000,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
       case Keys.Enter:
         event.preventDefault()
         event.stopPropagation()
-        if (data.comboboxState === ComboboxState.Closed) {
-          flushSync(() => actions.openCombobox())
+        if (machine.state.comboboxState === ComboboxState.Closed) {
+          flushSync(() => machine.actions.openCombobox())
         }
         refocusInput()
         return
@@ -1535,9 +1009,10 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
       case Keys.ArrowDown:
         event.preventDefault()
         event.stopPropagation()
-        if (data.comboboxState === ComboboxState.Closed) {
-          flushSync(() => actions.openCombobox())
-          if (!data.value) actions.goToOption(Focus.First)
+        if (machine.state.comboboxState === ComboboxState.Closed) {
+          flushSync(() => machine.actions.openCombobox())
+          if (!machine.state.dataRef.current.value)
+            machine.actions.goToOption({ focus: Focus.First })
         }
         refocusInput()
         return
@@ -1545,20 +1020,22 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
       case Keys.ArrowUp:
         event.preventDefault()
         event.stopPropagation()
-        if (data.comboboxState === ComboboxState.Closed) {
-          flushSync(() => actions.openCombobox())
-          if (!data.value) actions.goToOption(Focus.Last)
+        if (machine.state.comboboxState === ComboboxState.Closed) {
+          flushSync(() => machine.actions.openCombobox())
+          if (!machine.state.dataRef.current.value) {
+            machine.actions.goToOption({ focus: Focus.Last })
+          }
         }
         refocusInput()
         return
 
       case Keys.Escape:
-        if (data.comboboxState !== ComboboxState.Open) return
+        if (machine.state.comboboxState !== ComboboxState.Open) return
         event.preventDefault()
-        if (data.optionsElement && !data.optionsPropsRef.current.static) {
+        if (machine.state.optionsElement && !data.optionsPropsRef.current.static) {
           event.stopPropagation()
         }
-        flushSync(() => actions.closeCombobox())
+        flushSync(() => machine.actions.closeCombobox())
         refocusInput()
         return
 
@@ -1580,10 +1057,10 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
     // to preserve the focus of the `ComboboxInput`, we need to also check
     // that the `left` mouse button was clicked.
     if (event.button === MouseButton.Left) {
-      if (data.comboboxState === ComboboxState.Open) {
-        actions.closeCombobox()
+      if (machine.state.comboboxState === ComboboxState.Open) {
+        machine.actions.closeCombobox()
       } else {
-        actions.openCombobox()
+        machine.actions.openCombobox()
       }
     }
 
@@ -1597,26 +1074,31 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
   let { isHovered: hover, hoverProps } = useHover({ isDisabled: disabled })
   let { pressed: active, pressProps } = useActivePress({ disabled })
 
+  let [comboboxState, optionsElement] = useSlice(machine, (state) => [
+    state.comboboxState,
+    state.optionsElement,
+  ])
+
   let slot = useMemo(() => {
     return {
-      open: data.comboboxState === ComboboxState.Open,
-      active: active || data.comboboxState === ComboboxState.Open,
+      open: comboboxState === ComboboxState.Open,
+      active: active || comboboxState === ComboboxState.Open,
       disabled,
       invalid: data.invalid,
       value: data.value,
       hover,
       focus,
     } satisfies ButtonRenderPropArg
-  }, [data, hover, focus, active, disabled])
+  }, [data, hover, focus, active, disabled, comboboxState])
   let ourProps = mergeProps(
     {
       ref: buttonRef,
       id,
-      type: useResolveButtonType(props, data.buttonElement),
+      type: useResolveButtonType(props, localButtonElement),
       tabIndex: -1,
       'aria-haspopup': 'listbox',
-      'aria-controls': data.optionsElement?.id,
-      'aria-expanded': data.comboboxState === ComboboxState.Open,
+      'aria-controls': optionsElement?.id,
+      'aria-expanded': comboboxState === ComboboxState.Open,
       'aria-labelledby': labelledBy,
       disabled: disabled || undefined,
       autoFocus,
@@ -1677,8 +1159,8 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
     transition = false,
     ...theirProps
   } = props
+  let machine = useComboboxMachineContext('Combobox.Options')
   let data = useData('Combobox.Options')
-  let actions = useActions('Combobox.Options')
   let anchor = useResolvedAnchor(rawAnchor)
 
   // Always enable `portal` functionality, when `anchor` is enabled
@@ -1698,11 +1180,21 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
   let optionsRef = useSyncRefs(
     ref,
     anchor ? floatingRef : null,
-    actions.setOptionsElement,
+    machine.actions.setOptionsElement,
     setLocalOptionsElement
   )
-  let portalOwnerDocument = useOwnerDocument(data.buttonElement || data.inputElement)
-  let ownerDocument = useOwnerDocument(data.optionsElement)
+  let [comboboxState, inputElement, buttonElement, optionsElement, activationTrigger] = useSlice(
+    machine,
+    (state) => [
+      state.comboboxState,
+      state.inputElement,
+      state.buttonElement,
+      state.optionsElement,
+      state.activationTrigger,
+    ]
+  )
+  let portalOwnerDocument = useOwnerDocument(inputElement || buttonElement)
+  let ownerDocument = useOwnerDocument(optionsElement)
 
   let usesOpenClosedState = useOpenClosed()
   let [visible, transitionData] = useTransition(
@@ -1710,26 +1202,22 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
     localOptionsElement,
     usesOpenClosedState !== null
       ? (usesOpenClosedState & State.Open) === State.Open
-      : data.comboboxState === ComboboxState.Open
+      : comboboxState === ComboboxState.Open
   )
 
   // Ensure we close the combobox as soon as the input becomes hidden
-  useOnDisappear(visible, data.inputElement, actions.closeCombobox)
+  useOnDisappear(visible, inputElement, machine.actions.closeCombobox)
 
   // Enable scroll locking when the combobox is visible, and `modal` is enabled
-  let scrollLockEnabled = data.__demoMode
-    ? false
-    : modal && data.comboboxState === ComboboxState.Open
+  let scrollLockEnabled = data.__demoMode ? false : modal && comboboxState === ComboboxState.Open
   useScrollLock(scrollLockEnabled, ownerDocument)
 
   // Mark other elements as inert when the combobox is visible, and `modal` is enabled
-  let inertOthersEnabled = data.__demoMode
-    ? false
-    : modal && data.comboboxState === ComboboxState.Open
+  let inertOthersEnabled = data.__demoMode ? false : modal && comboboxState === ComboboxState.Open
   useInertOthers(inertOthersEnabled, {
     allowed: useCallback(
-      () => [data.inputElement, data.buttonElement, data.optionsElement],
-      [data.inputElement, data.buttonElement, data.optionsElement]
+      () => [inputElement, buttonElement, optionsElement],
+      [inputElement, buttonElement, optionsElement]
     ),
   })
 
@@ -1740,8 +1228,8 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
     data.optionsPropsRef.current.hold = hold
   }, [data.optionsPropsRef, hold])
 
-  useTreeWalker(data.comboboxState === ComboboxState.Open, {
-    container: data.optionsElement,
+  useTreeWalker(comboboxState === ComboboxState.Open, {
+    container: optionsElement,
     accept(node) {
       if (node.getAttribute('role') === 'option') return NodeFilter.FILTER_REJECT
       if (node.hasAttribute('role')) return NodeFilter.FILTER_SKIP
@@ -1752,19 +1240,19 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
     },
   })
 
-  let labelledBy = useLabelledBy([data.buttonElement?.id])
+  let labelledBy = useLabelledBy([buttonElement?.id])
 
   let slot = useMemo(() => {
     return {
-      open: data.comboboxState === ComboboxState.Open,
+      open: comboboxState === ComboboxState.Open,
       option: undefined,
     } satisfies OptionsRenderPropArg
-  }, [data.comboboxState])
+  }, [comboboxState])
 
   // When the user scrolls **using the mouse** (so scroll event isn't appropriate)
   // we want to make sure that the current activation trigger is set to pointer.
   let handleWheel = useEvent(() => {
-    actions.setActivationTrigger(ActivationTrigger.Pointer)
+    machine.actions.setActivationTrigger(ActivationTrigger.Pointer)
   })
 
   let handleMouseDown = useEvent((event: ReactMouseEvent) => {
@@ -1781,7 +1269,7 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
     // When the user clicks in the `<Options/>`, we want to make sure that we
     // set the activation trigger to `pointer` to prevent auto scrolling to the
     // active option while the user is scrolling.
-    actions.setActivationTrigger(ActivationTrigger.Pointer)
+    machine.actions.setActivationTrigger(ActivationTrigger.Pointer)
   })
 
   let ourProps = mergeProps(anchor ? getFloatingPanelProps() : {}, {
@@ -1793,10 +1281,10 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
     style: {
       ...theirProps.style,
       ...style,
-      '--input-width': useElementSize(data.inputElement, true).width,
-      '--button-width': useElementSize(data.buttonElement, true).width,
+      '--input-width': useElementSize(inputElement, true).width,
+      '--button-width': useElementSize(buttonElement, true).width,
     } as CSSProperties,
-    onWheel: data.activationTrigger === ActivationTrigger.Pointer ? undefined : handleWheel,
+    onWheel: activationTrigger === ActivationTrigger.Pointer ? undefined : handleWheel,
     onMouseDown: handleMouseDown,
     ...transitionDataAttributes(transitionData),
   })
@@ -1804,7 +1292,7 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
   // We should freeze when the combobox is visible but "closed". This means that
   // a transition is currently happening and the component is still visible (for
   // the transition) but closed from a functionality perspective.
-  let shouldFreeze = visible && data.comboboxState === ComboboxState.Closed
+  let shouldFreeze = visible && comboboxState === ComboboxState.Closed
 
   let options = useFrozenData(shouldFreeze, data.virtual?.options)
 
@@ -1814,18 +1302,19 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
   let isSelected = useEvent((compareValue) => data.compare(frozenValue, compareValue))
 
   // Map the children in a scrollable container when virtualization is enabled
-  if (data.virtual) {
+  let newDataContextValue = useMemo(() => {
+    if (!data.virtual) return data
     if (options === undefined) throw new Error('Missing `options` in virtual mode')
 
+    return options !== data.virtual.options
+      ? { ...data, virtual: { ...data.virtual, options } }
+      : data
+  }, [data, options, data.virtual?.options])
+
+  if (data.virtual) {
     Object.assign(theirProps, {
       children: (
-        <ComboboxDataContext.Provider
-          value={
-            options !== data.virtual.options
-              ? { ...data, virtual: { ...data.virtual, options } }
-              : data
-          }
-        >
+        <ComboboxDataContext.Provider value={newDataContextValue}>
           {/* @ts-expect-error The `children` prop now is a callback function that receives `{option}` */}
           <VirtualProvider slot={slot}>{theirProps.children}</VirtualProvider>
         </ComboboxDataContext.Provider>
@@ -1835,11 +1324,13 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
 
   let render = useRender()
 
+  let newData = useMemo(() => {
+    return data.mode === ValueMode.Multi ? data : { ...data, isSelected }
+  }, [data, isSelected])
+
   return (
     <Portal enabled={portal ? props.static || visible : false} ownerDocument={portalOwnerDocument}>
-      <ComboboxDataContext.Provider
-        value={data.mode === ValueMode.Multi ? data : { ...data, isSelected }}
-      >
+      <ComboboxDataContext.Provider value={newData}>
         {render({
           ourProps,
           theirProps: {
@@ -1896,7 +1387,7 @@ function OptionFn<
   TType = Parameters<typeof ComboboxRoot>[0]['value'],
 >(props: ComboboxOptionProps<TTag, TType>, ref: Ref<HTMLElement>) {
   let data = useData('Combobox.Option')
-  let actions = useActions('Combobox.Option')
+  let machine = useComboboxMachineContext('Combobox.Option')
 
   let internalId = useId()
   let {
@@ -1907,14 +1398,14 @@ function OptionFn<
     ...theirProps
   } = props
 
-  let refocusInput = useRefocusableInput(data.inputElement)
+  let [inputElement] = useSlice(machine, (state) => [state.inputElement])
 
-  let active = data.virtual
-    ? data.activeOptionIndex === data.calculateIndex(value)
-    : data.activeOptionIndex === null
-      ? false
-      : data.options[data.activeOptionIndex]?.id === id
+  let refocusInput = useRefocusableInput(inputElement)
 
+  let active = useSlice(
+    machine,
+    useCallback((state) => machine.selectors.isActive(state, value, id), [value, id])
+  )
   let selected = data.isSelected(value)
   let internalOptionRef = useRef<HTMLElement | null>(null)
 
@@ -1933,35 +1424,22 @@ function OptionFn<
   )
 
   let select = useEvent(() => {
-    actions.setIsTyping(false)
-    actions.onChange(value)
+    machine.actions.setIsTyping(false)
+    machine.actions.onChange(value)
   })
-  useIsoMorphicEffect(() => actions.registerOption(id, bag), [bag, id])
+  useIsoMorphicEffect(() => machine.actions.registerOption(id, bag), [bag, id])
 
-  let enableScrollIntoView = useRef(data.virtual || data.__demoMode ? false : true)
-  useIsoMorphicEffect(() => {
-    if (data.virtual) return
-    if (data.__demoMode) return
-    return disposables().requestAnimationFrame(() => {
-      enableScrollIntoView.current = true
-    })
-  }, [data.virtual, data.__demoMode])
+  let shouldScrollIntoView = useSlice(
+    machine,
+    useCallback((state) => machine.selectors.shouldScrollIntoView(state, value, id), [value, id])
+  )
 
   useIsoMorphicEffect(() => {
-    if (!enableScrollIntoView.current) return
-    if (data.comboboxState !== ComboboxState.Open) return
-    if (!active) return
-    if (data.activationTrigger === ActivationTrigger.Pointer) return
+    if (!shouldScrollIntoView) return
     return disposables().requestAnimationFrame(() => {
       internalOptionRef.current?.scrollIntoView?.({ block: 'nearest' })
     })
-  }, [
-    internalOptionRef,
-    active,
-    data.comboboxState,
-    data.activationTrigger,
-    /* We also want to trigger this when the position of the active item changes so that we can re-trigger the scrollIntoView */ data.activeOptionIndex,
-  ])
+  }, [shouldScrollIntoView, internalOptionRef])
 
   let handleMouseDown = useEvent((event: ReactMouseEvent<HTMLButtonElement>) => {
     // We use the `mousedown` event here since it fires before the focus event,
@@ -1997,16 +1475,16 @@ function OptionFn<
     }
 
     if (data.mode === ValueMode.Single) {
-      actions.closeCombobox()
+      machine.actions.closeCombobox()
     }
   })
 
   let handleFocus = useEvent(() => {
     if (disabled) {
-      return actions.goToOption(Focus.Nothing)
+      return machine.actions.goToOption({ focus: Focus.Nothing })
     }
     let idx = data.calculateIndex(value)
-    actions.goToOption(Focus.Specific, idx)
+    machine.actions.goToOption({ focus: Focus.Specific, idx })
   })
 
   let pointer = useTrackedPointer()
@@ -2018,7 +1496,7 @@ function OptionFn<
     if (disabled) return
     if (active) return
     let idx = data.calculateIndex(value)
-    actions.goToOption(Focus.Specific, idx, ActivationTrigger.Pointer)
+    machine.actions.goToOption({ focus: Focus.Specific, idx }, ActivationTrigger.Pointer)
   })
 
   let handleLeave = useEvent((evt) => {
@@ -2026,7 +1504,7 @@ function OptionFn<
     if (disabled) return
     if (!active) return
     if (data.optionsPropsRef.current.hold) return
-    actions.goToOption(Focus.Nothing)
+    machine.actions.goToOption({ focus: Focus.Nothing })
   })
 
   let slot = useMemo(() => {
diff --git a/packages/@headlessui-vue/package.json b/packages/@headlessui-vue/package.json
index 0dd685e..14ce885 100644
--- a/packages/@headlessui-vue/package.json
+++ b/packages/@headlessui-vue/package.json
@@ -50,6 +50,6 @@
     "vue": "3.2.37"
   },
   "dependencies": {
-    "@tanstack/vue-virtual": "3.0.0-beta.60"
+    "@tanstack/vue-virtual": "3.13.6"
   }
 }
diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.ts b/packages/@headlessui-vue/src/components/combobox/combobox.ts
index 27b2a1c..b5149f2 100644
--- a/packages/@headlessui-vue/src/components/combobox/combobox.ts
+++ b/packages/@headlessui-vue/src/components/combobox/combobox.ts
@@ -151,6 +151,8 @@ let VirtualProvider = defineComponent({
     })
 
     let virtualizer = useVirtualizer<HTMLDivElement, HTMLLIElement>(
+      // @ts-expect-error TODO: Drop when using `pnpm` and `@tanstack/virtual-vue`
+      // has been rolled back to the older version.
       computed(() => {
         return {
           scrollPaddingStart: padding.value.start,
@@ -173,6 +175,8 @@ let VirtualProvider = defineComponent({
       baseKey.value += 1
     })
 
+    // @ts-expect-error TODO: Drop when using `pnpm` and `@tanstack/virtual-vue`
+    // has been rolled back to the older version.
     provide(VirtualContext, api.virtual.value ? virtualizer : null)
 
     return () => {
diff --git a/patches/@tanstack+virtual-core+3.13.6.patch b/patches/@tanstack+virtual-core+3.13.6.patch
new file mode 100644
index 0000000..0f1c0f6
--- /dev/null
+++ b/patches/@tanstack+virtual-core+3.13.6.patch
@@ -0,0 +1,51 @@
+diff --git a/node_modules/@tanstack/virtual-core/dist/cjs/index.cjs b/node_modules/@tanstack/virtual-core/dist/cjs/index.cjs
+index dd4e77f..127987e 100644
+--- a/node_modules/@tanstack/virtual-core/dist/cjs/index.cjs
++++ b/node_modules/@tanstack/virtual-core/dist/cjs/index.cjs
+@@ -665,9 +665,9 @@ class Virtualizer {
+             this.options.getItemKey(index)
+           );
+           if (elementInDOM) {
+-            const [latestOffset] = utils.notUndefined(
+-              this.getOffsetForIndex(index, align)
+-            );
++            const result = this.getOffsetForIndex(index, align)
++            if (!result) return
++            const [latestOffset] = result
+             if (!utils.approxEqual(latestOffset, this.getScrollOffset())) {
+               this.scrollToIndex(index, { align, behavior });
+             }
+diff --git a/node_modules/@tanstack/virtual-core/dist/esm/index.js b/node_modules/@tanstack/virtual-core/dist/esm/index.js
+index 8da519d..8c78af8 100644
+--- a/node_modules/@tanstack/virtual-core/dist/esm/index.js
++++ b/node_modules/@tanstack/virtual-core/dist/esm/index.js
+@@ -663,9 +663,9 @@ class Virtualizer {
+             this.options.getItemKey(index)
+           );
+           if (elementInDOM) {
+-            const [latestOffset] = notUndefined(
+-              this.getOffsetForIndex(index, align)
+-            );
++            const result = this.getOffsetForIndex(index, align)
++            if (!result) return
++            const [latestOffset] = result
+             if (!approxEqual(latestOffset, this.getScrollOffset())) {
+               this.scrollToIndex(index, { align, behavior });
+             }
+diff --git a/node_modules/@tanstack/virtual-core/src/index.ts b/node_modules/@tanstack/virtual-core/src/index.ts
+index 3a0c446..4a9e792 100644
+--- a/node_modules/@tanstack/virtual-core/src/index.ts
++++ b/node_modules/@tanstack/virtual-core/src/index.ts
+@@ -1003,9 +1003,9 @@ export class Virtualizer<
+         )
+ 
+         if (elementInDOM) {
+-          const [latestOffset] = notUndefined(
+-            this.getOffsetForIndex(index, align),
+-          )
++          const result = this.getOffsetForIndex(index, align)
++          if (!result) return
++          const [latestOffset] = result
+ 
+           if (!approxEqual(latestOffset, this.getScrollOffset())) {
+             this.scrollToIndex(index, { align, behavior })
diff --git a/playgrounds/react/pages/combobox/combobox-countries.tsx b/playgrounds/react/pages/combobox/combobox-countries.tsx
index 4afa861..045c07e 100644
--- a/playgrounds/react/pages/combobox/combobox-countries.tsx
+++ b/playgrounds/react/pages/combobox/combobox-countries.tsx
@@ -75,7 +75,7 @@ export default function Home() {
               <Combobox.Options
                 transition
                 anchor="bottom start"
-                className="focus:outline-hidden data-closed:opacity-0 w-[calc(var(--input-width)+var(--button-width))] overflow-auto rounded-md bg-white py-1 text-base leading-6 shadow-lg transition duration-1000 [--anchor-gap:--spacing(1)] [--anchor-max-height:--spacing(60)] sm:text-sm sm:leading-5"
+                className="focus:outline-hidden data-closed:opacity-0 w-[calc(var(--input-width)+var(--button-width))] overflow-auto rounded-md bg-white py-1 text-base leading-6 shadow-lg transition duration-300 [--anchor-gap:--spacing(1)] [--anchor-max-height:--spacing(60)] sm:text-sm sm:leading-5"
               >
                 {countries.map((country) => (
                   <Combobox.Option


Test patch:
diff --git a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts
index 433fddd..17f1f19 100644
--- a/packages/@headlessui-vue/src/components/combobox/combobox.test.ts
+++ b/packages/@headlessui-vue/src/components/combobox/combobox.test.ts
@@ -4710,7 +4710,16 @@ describe.each([{ virtual: true }, { virtual: false }])(
   }
 )
 
-describe.each([{ virtual: true }, { virtual: false }])('Mouse interactions %s', ({ virtual }) => {
+// TODO: Re-enable virtual tests once we migrated from `npm` to `pnpm` and
+// rolled back the `@tanstack/virtual-vue` version.
+//
+// We had to bump `@tanstack/virtual-vue` such that the `@tanstack/virtual-core`
+// version was the same _and_ hoisted such that we could write a patch for it.
+// Different versions meant that the `@tanstack/virtual-core` version was
+// embedded in
+// `node_modules/@tanstack/virtual-react/node_modules/@tanstack/virtual-core`
+// which wasn't patchable via patch-package. Pnpm will solve this.
+describe.each([{ virtual: false }, { virtual: false }])('Mouse interactions %s', ({ virtual }) => {
   let data = ['Option A', 'Option B', 'Option C']
   let MyCombobox = defineComponent({
     components: getDefaultComponents(),