Skip to content

[combobox] Add built-in virtualization support - #5173

Open
michaldudak wants to merge 62 commits into
mui:masterfrom
michaldudak:combobox-virtualization
Open

[combobox] Add built-in virtualization support#5173
michaldudak wants to merge 62 commits into
mui:masterfrom
michaldudak:combobox-virtualization

Conversation

@michaldudak

@michaldudak michaldudak commented Jul 6, 2026

Copy link
Copy Markdown
Member

Summary

Adds built-in virtualization support to Combobox through a new <Combobox.Virtualizer> part backed by @mui/x-virtualizer.

The adapter preserves the existing Combobox filtering, keyboard navigation, selection, and accessibility behavior while mounting only the visible and overscanned items. The existing <Combobox.Root virtualized> prop remains available for integrations with other virtualization libraries.

What changed

  • Added and exported <Combobox.Virtualizer>.
  • Added @mui/x-virtualizer as a dependency of @base-ui/react.
  • Adapted the filtered Combobox item collection into stable MUI Virtualizer row models.
  • Added estimated and dynamic row-height support, pixel-based overscan, and runtime virtualization toggling.
  • Kept logical item indexes separate from the mounted DOM window so keyboard navigation and selection continue to work for offscreen items.
  • Kept the highlighted offscreen item mounted in a layout-neutral focus proxy for aria-activedescendant and Enter-key selection.
  • Added keyboard scrolling that:
    • respects CSS scroll padding,
    • corrects estimated positions after rows are measured,
    • aligns items taller than the viewport to its start,
    • does not scroll for pointer highlights.
  • Added isItemDisabled to make disabled state available before an item is mounted, allowing navigation to skip disabled offscreen items.
  • Added stable key handling through getItemKey, which is required for object values and unknown item types.
  • Added list-level coordination for temporarily rendering every row when browser autofill needs rendered labels.
  • Preserved the external virtualization path and clarified that the virtualized root prop is not used with the built-in virtualizer.
  • Updated the public virtualized Combobox demos and documentation.
  • Added a private side-by-side experiment comparing the built-in implementation with the previous @tanstack/react-virtual integration: /experiments/combobox/virtualizer.

Current limitations

  • Requires the items prop on <Combobox.Root>.
  • Supports flat listbox collections only.
  • Grouped collections and grid mode are not currently supported.
  • The virtualizer must be the only item-rendering child of <Combobox.List>.
  • Each item renderer must return exactly one <Combobox.Item>.
  • The virtualizer element must have a constrained height or maximum height.

Docs

Validation

  • pnpm test:chromium ComboboxVirtualizer --no-watch
  • pnpm test:jsdom Combobox --no-watch
  • pnpm exec tsgo -b tsconfig.json --force
  • pnpm prettier
  • pnpm eslint
  • pnpm stylelint
  • pnpm --filter @base-ui/react build
  • Manually verified both implementations in the private comparison experiment: opening the dropdown, rendering a virtual window, filtering 10,000 items, keyboard selection, and closing after selection.

@michaldudak michaldudak added type: new feature Expand the scope of the product to solve a new problem. component: combobox Changes related to the combobox component. labels Jul 6, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jul 6, 2026

Copy link
Copy Markdown

commit: 01a466a

@code-infra-dashboard

code-infra-dashboard Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bundle size

Bundle Parsed size Gzip size
@base-ui/react 🔺+59.3KB(+13.21%) 🔺+19.4KB(+13.24%)

Details of bundle changes

Performance

Total duration: 1,061.19 ms -66.78 ms(-5.9%) | Renders: 78 (+0) | Paint: 1,639.53 ms -127.96 ms(-7.2%)

No significant changes — details


Check out the code infra dashboard for more information about this PR.

@netlify

netlify Bot commented Jul 6, 2026

Copy link
Copy Markdown

Deploy Preview for base-ui ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit 01a466a
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/6a71cd4ae61b6b000856852e
😎 Deploy Preview https://deploy-preview-5173--base-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 10, 2026
@michaldudak
michaldudak force-pushed the combobox-virtualization branch from be88837 to 37da8af Compare July 15, 2026 10:42
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jul 15, 2026
@michaldudak
michaldudak force-pushed the combobox-virtualization branch from 37da8af to ddc0169 Compare July 15, 2026 14:15
@github-actions github-actions Bot added PR: out-of-date The pull request has merge conflicts and can't be merged. and removed PR: out-of-date The pull request has merge conflicts and can't be merged. labels Jul 16, 2026
@michaldudak michaldudak changed the title [combobox] Add built-in virtualization support [combobox][select] Add built-in virtualization support Jul 21, 2026
@michaldudak michaldudak added the component: select Changes related to the select component. label Jul 21, 2026
@github-actions github-actions Bot added PR: out-of-date The pull request has merge conflicts and can't be merged. and removed PR: out-of-date The pull request has merge conflicts and can't be merged. labels Jul 27, 2026
@michaldudak michaldudak changed the title [combobox][select] Add built-in virtualization support [combobox] Add built-in virtualization support Jul 30, 2026
@michaldudak michaldudak removed the component: select Changes related to the select component. label Jul 30, 2026
@michaldudak
michaldudak marked this pull request as ready for review July 31, 2026 10:48

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dedd657ace

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/react/src/internals/virtualization/ListVirtualizerAdapter.tsx Outdated
Comment thread packages/react/src/internals/virtualization/ListVirtualizer.tsx Outdated
Comment thread docs/src/app/(docs)/react/components/combobox/page.mdx Outdated
Comment thread packages/react/src/internals/virtualization/ListVirtualizerAdapter.tsx Outdated
Comment thread packages/react/src/combobox/root/AriaCombobox.tsx Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eac7cfcdeb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/react/src/combobox/root/AriaCombobox.tsx Outdated
michaldudak and others added 2 commits August 3, 2026 10:59

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aaa50a5d2f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/react/src/combobox/root/AriaCombobox.tsx
Comment on lines +617 to +618
if (builtInVirtualizer) {
builtInVirtualizer.resetScroll();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the filter-triggered scroll reset

When a keyboard-highlighted virtualized list is filtered and the active index remains in bounds, this reset is immediately undone. Filtering changes the row ID at that index, so ListVirtualizer's scrollToRowId layout effect treats it as a new highlighted-row request and scrolls the same numerical index back into view. For example, after navigating to index 50 in a 1,000-item list, typing a query that leaves more than 50 matches resets to 0 here and then jumps to filtered index 50, hiding the first results. The filter reset needs to cancel or suppress that highlighted-row scroll until navigation changes again.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not reproducible — the premise that the active index survives the filter does not hold.

When the user types, ComboboxInput clears the highlight unless autoHighlight is enabled (ComboboxInput.tsx, onChange: if (open && store.state.activeIndex !== null && !autoHighlightEnabled) clearHighlight()). With autoHighlight on, the highlight is deliberately kept but setInputValue immediately re-points it at getFirstEnabledIndex(...) of the new collection.

Either way scrollToRowIndex no longer refers to the pre-filter index by the time ListVirtualizer re-renders, so the scrollToRowId layout effect has nothing to scroll back to and the reset stands.

Verified with a test that navigates to index 10 in a 100-item virtualized list, types a query leaving 20 matches (so the old index stays in bounds), and asserts the scroller is still at 0 afterwards. It passes without any change; it is now in ComboboxVirtualizer.test.tsx as keeps the virtual scroller reset when filtering a keyboard-highlighted list to guard the behavior.

Leaving this thread open in case I have missed a configuration where the highlight is retained across a query change.

michaldudak and others added 3 commits August 3, 2026 14:14
The block padding lived on `Combobox.List`, outside the scroll container,
so it framed the popup at every scroll position and inset the scrollbar.
Drop it, along with the `--available-height` compensation it required.
The TanStack demos keep their own spacing inside the virtual content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rows are painted in a sticky viewport nested inside absolutely positioned
content, and padding on the scrollport landed in three different coordinate
systems: the absolute content ignored it, the sticky viewport was pinned
against the content edge, and the scroll math mixed padded `scrollTop` with
unpadded row positions. Padded lists never painted rows in the padding, jumped
by the padding at the maximum scroll position, and pushed all of the space
below the items when the collection was short enough not to scroll.

Measure the block padding and treat it as part of the scroll geometry: the
absolute content spans it, the sticky viewport covers the whole scrollport,
and row positions convert to scroll offsets. `--total-size` now reports the
scrollable content size, so a border-box scrollport sized from it still fits
its rows exactly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Aug 3, 2026
Opening a virtualized list with an arrow key scanned only the DOM for the
first enabled item, so the first unmounted slot was picked as enabled and
`isItemDisabled` was ignored. The initial-open scan now consults the consumer
predicate alongside the DOM state, keeping the attribute-based skipping that
mui#2604 relies on.

A disabled `<Combobox.Virtualizer>` stayed registered with the list, which
suppressed the DOM `scrollIntoView` that static lists rely on while the
virtualizer itself scrolled no rows. The registry entry now carries whether
virtualization is enabled, and DOM scrolling is only suppressed while it is.

Also list `Combobox.Virtualizer` in the anatomy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6c8b6cb57d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/react/src/internals/virtualization/ListVirtualizer.tsx Outdated
michaldudak and others added 2 commits August 4, 2026 11:29
…zation

# Conflicts:
#	docs/src/app/(docs)/react/components/combobox/types.md
The adaptive estimate caches were rewritten during render, so a concurrent
render that React discards — a transition whose sibling suspends, for example —
still cleared the measurements and known row IDs belonging to the tree that
stayed committed.

The decision is still made during render, because the estimate it invalidates
is part of that render's geometry, but it is now applied in a layout effect and
the render-visible estimate is derived from the pure decision instead of the
ref it is about to clear.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Aug 4, 2026
The alignment test slept for exactly `DIRECT_INPUT_WINDOW_MS` after shrinking
the rows, so it sampled the geometry at the boundary of the refresh window it
was waiting on and failed on slower machines.

Shrinking the rows to the estimate makes the virtual total settle at exactly
`rows × estimate` once the last measured row is remeasured, which is the point
the alignment has to survive. Wait for that instead of for a delay.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: combobox Changes related to the combobox component. PR: out-of-date The pull request has merge conflicts and can't be merged. type: new feature Expand the scope of the product to solve a new problem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant