DOM Virtual List Combobox Pattern
Problem
Virtualize a popup listbox (combobox/dropdown style) without hand-wiring Virtualizer attach, count updates, and teardown every time the popup opens or closes.
Runnable Example
The snippet below is copy-paste runnable in a TypeScript project with @vielzeug/virtualit installed.
Use createDomVirtualList so your component only needs to call setItems(items) and setActive(isOpen) with a DOM render callback.
ts
import { createDomVirtualList, type DomVirtualListController } from '@vielzeug/virtualit/dom';
type Option = { disabled?: boolean; label: string; value: string };
const options: Option[] = Array.from({ length: 2_000 }, (_, i) => ({
label: `Option ${i}`,
value: `opt-${i}`,
}));
let isOpen = false;
let focusedIndex = 0;
let controller: DomVirtualListController<Option> | null = null;
function ensureController() {
if (controller) return controller;
controller = createDomVirtualList<Option>({
estimateSize: 36,
getListElement: () => document.querySelector<HTMLElement>('[role="listbox"]'),
getScrollElement: () => document.querySelector<HTMLElement>('.dropdown'),
overscan: 4,
render: ({ items, listEl, virtualItems }) => {
for (const item of virtualItems) {
const opt = items[item.index];
if (!opt) continue;
const row = document.createElement('button');
row.type = 'button';
row.className = 'option';
row.style.cssText = `position:absolute;top:0;left:0;right:0;transform:translateY(${item.top}px);`;
row.textContent = opt.label;
row.disabled = !!opt.disabled;
row.addEventListener('click', () => {
console.log('selected', opt.value);
});
listEl.appendChild(row);
}
},
});
return controller;
}
function openDropdown() {
isOpen = true;
ensureController().setItems(options);
ensureController().setActive(isOpen);
}
function closeDropdown() {
isOpen = false;
ensureController().setActive(isOpen); // disables virtualization + resets list styles
}
function onArrowDown() {
focusedIndex = Math.min(focusedIndex + 1, options.length - 1);
ensureController().scrollToIndex(focusedIndex, { align: 'auto' });
}
function destroyCombobox() {
controller?.destroy();
controller = null;
}Expected Output
- Opening the dropdown only renders the visible option window plus overscan.
- Keyboard focus movement can keep the focused option visible with
scrollToIndex(..., { align: 'auto' }). - Closing and destroying cleanly tear down the virtualizer.
Common Pitfalls
- Returning
nullfromgetScrollElement/getListElementwhile open prevents activation. - Forgetting
setItems(items)orsetActive(isOpen)on open/close leaves stale DOM state. - Mixing non-virtual state rows with
.optionrows without a clear strategy can makeclearremove the wrong nodes.