Build a Custom Select / Dropdown That Beats Native

“Build a custom select” is a frontend machine-coding question that looks simple and is genuinely hard. The native <select> element is great for short lists and terrible for almost anything else — no styling, no search, no virtualization, no async loading. The interview tests whether you can rebuild it correctly with full accessibility.

Why native often fails

  • Cannot style the dropdown panel
  • No search or filter
  • No virtualization for large option lists
  • Limited custom rendering of options (no avatars, descriptions, multi-line)
  • iOS shows a wheel picker that is unusable for long lists
  • No async loading of options
  • No multi-select friendly UX

Clarify scope

  • Single or multi-select?
  • Sync or async options?
  • Search / filter required?
  • Custom rendering for each option?
  • Mobile UX (full screen vs popover)?

The WAI-ARIA combobox pattern

This is the standard. Roles:

  • Wrapper has role="combobox"
  • Trigger button or input has aria-expanded, aria-controls, aria-activedescendant
  • Listbox has role="listbox"
  • Options have role="option" and unique IDs

Focus stays in the input/trigger; aria-activedescendant indicates the highlighted option to assistive tech.

Keyboard interaction

  • Tab into the trigger
  • Enter / Space / ArrowDown — open the dropdown
  • ArrowUp / ArrowDown — navigate options
  • Enter — select highlighted option, close
  • Escape — close without selecting
  • Type-ahead — focus jumps to options starting with typed letters
  • Tab — close and move focus

Architecture

  • Trigger: button or input depending on whether you support search
  • Popover: positioned with Floating UI / Popper for collision detection
  • Options: virtualized if > 50 items
  • Selection: controlled by parent prop; component reports onChange

Positioning

  • Below trigger by default
  • Flip above if no room below (collision detection)
  • Match trigger width by default; allow override
  • Portal to document.body to escape overflow:hidden
  • Use Floating UI (formerly Popper.js) for the math

Search / filter

If options are large, add a search input inside the popover:

  • Filter options as the user types
  • Debounce only if the filter is expensive (synchronous string match is not)
  • Keep keyboard navigation working through filtered results
  • “No results” empty state

Async options

For options that come from a server (e.g., user picker):

  • Show loading state in the popover
  • Debounce the search query
  • Cancel in-flight requests on new query (AbortController)
  • Cache results per query string

Virtualization

  • react-window or TanStack Virtual
  • Adjust ARIA: aria-setsize and aria-posinset on each option so screen readers know the full list size
  • Scroll the highlighted option into view on keyboard navigation

Multi-select variant

  • Trigger shows selected items as chips (see the tags-input interview question)
  • Options have checkboxes
  • Stay open after selection by default; ESC to close
  • Keyboard: Space toggles selection; Enter closes

Mobile UX

  • On small screens, expand to a full-screen sheet instead of popover
  • Search input at top with keyboard available
  • Native-style “Done” button to close
  • Larger touch targets (44px minimum)

Custom rendering

  • Option can include avatar, description, badges
  • Render prop or template per option
  • Selected display in trigger may differ from option display

Edge cases

  • User clicks outside — close (mouseup outside, not mousedown, to avoid swallowing)
  • User scrolls page while open — re-position popover
  • Trigger element scrolled out of view — close
  • Window resizes — recompute position
  • RTL languages — flip arrow keys
  • Focus trap inside popover for screen-reader users

Accessibility validation

Test with NVDA / VoiceOver / TalkBack:

  • Trigger announces “combobox, collapsed”
  • Open: “expanded, X options”
  • Navigate: “Option 2 of 5: Apple”
  • Select: “Selected Apple”

What separates senior from staff

Senior implementations get the basic combobox pattern right. Staff implementations handle async options, virtualization, the multi-select variant, and the mobile-sheet adaptation. Principal-level discussion covers focus management edge cases, RTL, and the API design for headless vs styled components.

Frequently Asked Questions

Library options for production?

downshift (the most flexible headless), react-select (popular but opinionated), Combobox from Radix UI (modern). Headless UI from Tailwind is a solid default in 2026.

Can I just use the native select on mobile?

Yes for short lists (under 20 options). For long lists or with search, build custom. The native iOS wheel picker is hostile for any list longer than 10 items.

How do I handle 10,000 options?

Server-side filtering on every keystroke; only render the top 50 results. Debounce. The user is searching, not browsing.

Scroll to Top