“Build a virtualized autocomplete” extends the search-as-you-type question to handle large datasets — 10,000 countries, 100,000 products, or any list too big to render in DOM. The interview tests whether you understand virtualization, complex ARIA patterns, and the performance characteristics of list rendering.
Functional requirements
- Filter as the user types (or pull filtered results from API)
- Render up to 100,000 visible options without performance degradation
- Smooth scroll through results
- Keyboard navigation
- Selected option scrolls into view
- Screen-reader friendly
Why virtualize?
Rendering 10,000 DOM nodes drops to single-digit fps and uses hundreds of MB of memory. Virtualization renders only ~10–20 visible items + a small overscan buffer.
Libraries: react-window (lightweight), react-virtualized (more features), TanStack Virtual (modern, framework-agnostic).
The basic pattern
- Container has fixed height with overflow scroll
- Inner sizer has total height = N × itemHeight
- Visible items are absolutely positioned at the right offset
- onScroll updates which items are visible
Filtering
Two strategies:
- Client-side: entire list in memory, filtered with includes() or fuzzy match. Works for <100K items if data is preloaded.
- Server-side: API returns only matching items per query. Required for very large or sensitive datasets.
For interview answers, default to client-side unless the dataset is huge.
Variable-height items
If items are different heights (e.g., some have descriptions), measure them dynamically:
- Use ResizeObserver per row
- Cache heights as items render
- Adjust scroll position when measurements change
Modern libraries handle this — use them rather than hand-rolling.
Keyboard navigation with virtualization
Tricky: highlighted item may be off-screen. When user presses Down past the visible window:
- Update selectedIndex
- Scroll the container so the new selectedIndex is visible
- Update aria-activedescendant
Use scrollIntoView({ block: ‘nearest’ }) for the simplest correct behavior.
ARIA pattern
The combobox + listbox pattern:
- Input has role=”combobox”, aria-expanded, aria-controls
- Listbox has role=”listbox”
- Visible options have role=”option” and aria-selected
- aria-activedescendant points to the highlighted option ID
Critical: aria-activedescendant works even when the focused element is the input. The screen reader announces the active descendant. Don’t move keyboard focus to the option itself.
Performance budget
- Initial render: <50ms for 10K items
- Scroll: 60fps even when scrolling rapidly
- Filter: <100ms after debounce fires
Common mistakes
- Virtualizing a 200-item list (overkill, just render them all)
- Variable-height items without measurement (jumpy scroll)
- Setting tabIndex on every option (breaks keyboard nav)
- Filtering on every keystroke without debounce (jank)
Frequently Asked Questions
How do I handle highlighted item being filtered out?
When filter changes, reset selectedIndex to 0 (first visible item). Or preserve selection by ID and find new index after filter.
Should I use react-window or react-virtualized?
react-window for simple use cases (lighter, smaller bundle). react-virtualized for complex (variable height, masonry, infinite loaders).
How does this work with mobile touch scrolling?
Same as any scrolling list. Overscroll behavior may need overscroll-behavior: contain to prevent body scroll bleeding through.