Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
f711186
Shared primitives
michaldudak Jul 6, 2026
e2653fd
Create a ComboboxVirtualizer part
michaldudak Jul 6, 2026
3ef71d5
Docs
michaldudak Jul 6, 2026
adbdade
[combobox] Fix virtualizer transform test
michaldudak Jul 6, 2026
ba7b828
[combobox] Use MUI X virtualizer
michaldudak Jul 15, 2026
aec3ea4
Improvements
michaldudak Jul 16, 2026
b0892c5
Dedupe
michaldudak Jul 16, 2026
e7d795b
Fixes
michaldudak Jul 16, 2026
a22defb
Experiment
michaldudak Jul 16, 2026
0633a57
Fixes
michaldudak Jul 16, 2026
ac3d285
Extract generic ListVirtualizer
michaldudak Jul 16, 2026
529a980
Mention third-party virtualizers in the docs
michaldudak Jul 17, 2026
6087242
Stabilize the tests
michaldudak Jul 17, 2026
ec829d2
[combobox] Fix React 18 virtualization CI
michaldudak Jul 17, 2026
6241cbd
[combobox] Fix virtualizer regressions
michaldudak Jul 17, 2026
d966e6d
Perf improvements
michaldudak Jul 20, 2026
79e9592
Remove padding props
michaldudak Jul 20, 2026
0252233
Rename props
michaldudak Jul 20, 2026
4beca01
Cleanup
michaldudak Jul 20, 2026
bd78910
MUI X -> MUI X Virtualizer
michaldudak Jul 20, 2026
471d7c6
Move VirtualizationStore to combobox store
michaldudak Jul 20, 2026
fd56b19
Implement virtualization in Select
michaldudak Jul 21, 2026
8bdfa88
[select] Fix virtualized labels and alignment
michaldudak Jul 22, 2026
f7934d2
[select] Prevent pointer highlight scrolling
michaldudak Jul 22, 2026
7a4ba17
[select] Export collection item types
michaldudak Jul 22, 2026
9509b4c
[select][combobox] Combine item disabled states
michaldudak Jul 22, 2026
a4cfcaf
[select][combobox] Stabilize virtualizer row identity
michaldudak Jul 22, 2026
d63d25e
[select] Warn when items mismatch rendered items
michaldudak Jul 22, 2026
bbe2f0e
[select][combobox] Share virtualizer adapter
michaldudak Jul 22, 2026
2623edc
[select][combobox] Add virtualizer scroll action
michaldudak Jul 22, 2026
3668c31
[select][combobox] Simplify virtualizer internals
michaldudak Jul 22, 2026
8bdf85e
[select] Compute initial selected index lazily
michaldudak Jul 22, 2026
b0ca69f
[select][combobox] Fix virtualization CI and autofill
michaldudak Jul 23, 2026
3826f1a
[select][combobox] Fix offscreen virtualized items
michaldudak Jul 23, 2026
337c341
Use variable height rows in the experiment
michaldudak Jul 23, 2026
4eb7685
[combobox][select] Fix adaptive virtualizer scrolling
michaldudak Jul 27, 2026
dc07fee
[combobox][select] Preserve adaptive estimate across filtering
michaldudak Jul 27, 2026
55f3139
Fix getItemKey invalidation bug
michaldudak Jul 27, 2026
8f494b2
[combobox] Keep selected virtual row visible
michaldudak Jul 27, 2026
327f991
Dedupe
michaldudak Jul 29, 2026
7e18879
[combobox][select] Fix last row clipping during scrollbar drags
michaldudak Jul 29, 2026
a04f938
[combobox][select] Keep virtualized keyboard highlights visible
michaldudak Jul 29, 2026
2301e7e
Simplify
michaldudak Jul 29, 2026
fdd2ed4
[combobox] Avoid false unconstrained virtualizer warning
michaldudak Jul 29, 2026
1964f3e
[combobox][select] Unmark demoted rows as measured
michaldudak Jul 29, 2026
a88a51a
Refer to the virtualizer as MUI Virtualizer
michaldudak Jul 30, 2026
d7c30cc
[combobox][select] Keep the viewport covered and anchored through geo…
michaldudak Jul 30, 2026
bfb8926
Merge remote-tracking branch 'upstream/master' into combobox-virtuali…
michaldudak Jul 30, 2026
487a164
[combobox][select] Fix virtualization CI failures
michaldudak Jul 30, 2026
2bdc317
Clean up the docs
michaldudak Jul 31, 2026
dedd657
[select] Remove virtualization stress test
michaldudak Jul 31, 2026
4747f8e
[combobox][select] Stabilize restored virtual selection
michaldudak Jul 31, 2026
eac7cfc
[combobox] Fix virtualization review issues
michaldudak Jul 31, 2026
068e69b
Perf improvements
michaldudak Aug 3, 2026
aaa50a5
[combobox] Add non-virtualized list to virtualizer experiment
michaldudak Aug 3, 2026
b038607
Fix flaky tests
michaldudak Aug 3, 2026
6633a4c
[combobox] Remove popup padding from virtualized demos
michaldudak Aug 3, 2026
fd74313
[combobox] Support scrollport padding in the virtualizer
michaldudak Aug 3, 2026
6c8b6cb
[combobox][composite] Fix virtualized disabled items and scrolling
michaldudak Aug 4, 2026
daf4957
Merge remote-tracking branch 'upstream/master' into combobox-virtuali…
michaldudak Aug 4, 2026
46934a5
[combobox] Apply adaptive cache resets in a layout effect
michaldudak Aug 4, 2026
01a466a
[combobox] Wait for the geometry rewrite instead of a fixed delay
michaldudak Aug 4, 2026
e0fae07
[combobox] Replace Combobox.Virtualizer with a public ListVirtualizer
michaldudak Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Doesn't render its own HTML element.
| onOpenChangeComplete | `((open: boolean) => void)` | - | Event handler called after any animations complete when the popup is opened or closed. |
| openOnInputClick | `boolean` | `false` | Whether the popup opens when clicking the input. |
| submitOnItemClick | `boolean` | `false` | Whether clicking an item should submit the autocomplete's owning form.
By default, clicking an item via a pointer or Enter key does not submit the owning form.
Useful when the autocomplete is used as a single-field form search input. |
| virtualized | `boolean` | `false` | Whether the items are being externally virtualized. |
| virtualized | `boolean` | `false` | Whether the items are being virtualized by an external, third-party virtualizer. |
| disabled | `boolean` | `false` | Whether the component should ignore user interaction. |
| readOnly | `boolean` | `false` | Whether the user should be unable to choose a different option from the popup. |
| required | `boolean` | `false` | Whether the user must choose a value before submitting a form. |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
.Input {
box-sizing: border-box;
padding: 0 0.5rem;
margin: 0;
border-radius: 0;
border: 1px solid oklch(14.5% 0 0deg);
width: 16rem;
height: 2rem;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 400;
background-color: white;
color: oklch(14.5% 0 0deg);
outline: none;

@media (any-pointer: coarse) {
font-size: 1rem;
line-height: 1.5rem;
}

@media (prefers-color-scheme: dark) {
border: 1px solid white;
background-color: oklch(14.5% 0 0deg);
color: white;
}

&:focus {
outline: 2px solid oklch(14.5% 0 0deg);
outline-offset: -1px;

@media (prefers-color-scheme: dark) {
outline-color: white;
}
}
}

.Label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 700;
color: oklch(14.5% 0 0deg);

@media (prefers-color-scheme: dark) {
color: white;
}
}

.Positioner {
outline: 0;
}

.Popup {
box-sizing: border-box;
background-color: white;
color: oklch(14.5% 0 0deg);
width: var(--anchor-width);
max-width: var(--available-width);
border: 1px solid oklch(14.5% 0 0deg);
box-shadow: 0.25rem 0.25rem 0 rgb(0 0 0 / 12%);

@media (prefers-color-scheme: dark) {
background-color: oklch(14.5% 0 0deg);
color: white;
border: 1px solid white;
box-shadow: none;
}
}

.Scroller {
box-sizing: border-box;
height: min(22.5rem, var(--total-size));
max-height: var(--available-height);
overflow: auto;
overscroll-behavior: contain;
scroll-padding-block: 0.25rem;
}

.VirtualizedPlaceholder {
width: 100%;
position: relative;
}

.List {
box-sizing: border-box;
padding: 0;
}

.Item {
box-sizing: border-box;
outline: 0;
cursor: default;
-webkit-user-select: none;
user-select: none;
padding-block: 0.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
display: grid;
gap: 0.5rem;
align-items: center;
grid-template-columns: 1rem 1fr;
font-size: 0.875rem;
line-height: 1rem;

&[data-highlighted] {
z-index: 0;
position: relative;
color: white;

@media (prefers-color-scheme: dark) {
color: oklch(14.5% 0 0deg);
}
}

&[data-highlighted]::before {
content: '';
z-index: -1;
position: absolute;
inset-block: 0;
inset-inline: 0;
background-color: oklch(14.5% 0 0deg);

@media (prefers-color-scheme: dark) {
background-color: white;
}
}
}

.ItemText {
grid-column-start: 2;
}

.ItemIndicator {
grid-column-start: 1;
}

.Empty {
box-sizing: border-box;
font-size: 0.875rem;
line-height: 1rem;
color: oklch(55.6% 0 0deg);
padding: 0.75rem 0.5rem;

@media (prefers-color-scheme: dark) {
color: oklch(70.8% 0 0deg);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
'use client';
import * as React from 'react';
import { Combobox } from '@base-ui/react/combobox';
import { useVirtualizer } from '@tanstack/react-virtual';
import styles from './index.module.css';

export default function ExampleVirtualizedCombobox() {
const virtualizerRef = React.useRef<Virtualizer | null>(null);

return (
<Combobox.Root
virtualized
items={virtualizedItems}
itemToStringLabel={getItemLabel}
onItemHighlighted={(item, { reason, index }) => {
const virtualizer = virtualizerRef.current;

if (!item || !virtualizer) {
return;
}

const isStart = index === 0;
const isEnd = index === virtualizer.options.count - 1;
const shouldScroll = reason === 'none' || (reason === 'keyboard' && (isStart || isEnd));

if (shouldScroll) {
queueMicrotask(() => {
virtualizer.scrollToIndex(index, { align: isEnd ? 'start' : 'end' });
});
}
}}
>
<label className={styles.Label}>
Search 10,000 items
<Combobox.Input className={styles.Input} />
</label>

<Combobox.Portal>
<Combobox.Positioner className={styles.Positioner} sideOffset={4}>
<Combobox.Popup className={styles.Popup}>
<Combobox.Empty>
<div className={styles.Empty}>No items found.</div>
</Combobox.Empty>
<Combobox.List className={styles.List}>
<VirtualizedList virtualizerRef={virtualizerRef} />
</Combobox.List>
</Combobox.Popup>
</Combobox.Positioner>
</Combobox.Portal>
</Combobox.Root>
);
}

function VirtualizedList({
virtualizerRef,
}: {
virtualizerRef: React.RefObject<Virtualizer | null>;
}) {
const filteredItems = Combobox.useFilteredItems<DemoItem>();
const scrollElementRef = React.useRef<HTMLDivElement | null>(null);

const virtualizer = useVirtualizer({
count: filteredItems.length,
getScrollElement: () => scrollElementRef.current,
estimateSize: () => 32,
overscan: 20,
paddingStart: 4,
paddingEnd: 4,
scrollPaddingEnd: 4,
scrollPaddingStart: 4,
});

React.useImperativeHandle(virtualizerRef, () => virtualizer);

const handleScrollElementRef = React.useCallback(
(element: HTMLDivElement | null) => {
scrollElementRef.current = element;
if (element) {
virtualizer.measure();
}
},
[virtualizer],
);

const totalSize = virtualizer.getTotalSize();

if (filteredItems.length === 0) {
return null;
}

return (
<div
role="presentation"
ref={handleScrollElementRef}
className={styles.Scroller}
style={{ '--total-size': `${totalSize}px` } as React.CSSProperties}
>
<div
role="presentation"
className={styles.VirtualizedPlaceholder}
style={{ height: totalSize }}
>
{virtualizer.getVirtualItems().map((virtualItem) => {
const item = filteredItems[virtualItem.index];
if (!item) {
return null;
}

return (
<Combobox.Item
key={virtualItem.key}
index={virtualItem.index}
data-index={virtualItem.index}
ref={virtualizer.measureElement}
value={item}
className={styles.Item}
aria-setsize={filteredItems.length}
aria-posinset={virtualItem.index + 1}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: virtualItem.size,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<Combobox.ItemIndicator className={styles.ItemIndicator}>
<CheckIcon />
</Combobox.ItemIndicator>
<span className={styles.ItemText}>{item.name}</span>
</Combobox.Item>
);
})}
</div>
</div>
);
}

function CheckIcon(props: React.ComponentProps<'svg'>) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
{...props}
style={{ display: 'block', ...props.style }}
>
<path d="m2.5 8.5 4 4 7-9" />
</svg>
);
}

interface DemoItem {
id: string;
name: string;
}

function getItemLabel(item: DemoItem | null) {
return item ? item.name : '';
}

const virtualizedItems: DemoItem[] = Array.from({ length: 10000 }, (_, index) => {
const id = String(index + 1);
const indexLabel = id.padStart(4, '0');
return { id, name: `Item ${indexLabel}` };
});

type Virtualizer = ReturnType<typeof useVirtualizer<HTMLDivElement, Element>>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createDemoWithVariants } from 'docs/src/utils/createDemo';
import CssModules from './css-modules';
import Tailwind from './tailwind';

export const DemoComboboxVirtualizedTanStack = createDemoWithVariants(import.meta.url, {
CssModules,
Tailwind,
});
Loading
Loading