Build a Virtualized Autocomplete: Large Lists with Smooth Scroll

“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

  1. Container has fixed height with overflow scroll
  2. Inner sizer has total height = N × itemHeight
  3. Visible items are absolutely positioned at the right offset
  4. 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.

Scroll to Top