0) Problem Restatement
Roblox asked a frontend system design question: design a reusable, production-grade dropdown menu (select) for a design system used by many teams. It should be easy to use, flexible, accessible (keyboard and screen readers), fast with many options, and consistent across the product.
1) Requirements
- Single select and multi-select.
- Options from a static list or loaded asynchronously (search as you type).
- Optional search/filter box, groups and disabled options, and custom option rendering (icon + label).
- Keyboard: open/close, arrow navigation, Enter to select, Esc to close, type-ahead jump.
- Accessible to screen readers. Works on mobile.
- Handles 10,000+ options smoothly.
2) Public API (React)
<Select
options={options} // [{ value, label, disabled?, group? }]
value={value} // controlled (optional)
defaultValue={initial} // uncontrolled (optional)
onChange={(v) => setValue(v)}
multiple={false}
searchable
loadOptions={(query) => fetchUsers(query)} // async (optional)
renderOption={(opt, state) => <UserRow {...opt} active={state.active} />}
placeholder="Select a user"
disabled={false}
aria-label="Assignee"
/>
- Controlled vs uncontrolled: if
valueis passed, the parent owns the state (controlled). Otherwise the component keeps its own (uncontrolled, usingdefaultValue). Supporting both is standard for design systems. - Headless core + styled wrapper: a
useSelect()hook contains all logic (state, keyboard, ARIA props), and the<Select>component only renders. Teams with unusual UIs can use the hook directly.
3) Internal Structure
Architecture Diagram
flowchart LR
HOOK["useSelect hook - state machine"] --> TRIG["Trigger button / input"]
HOOK --> LIST["Listbox - rendered in a portal"]
LIST --> VIRT["Virtualized option rows"]
HOOK --> ASYNC["Async loader - debounce, cancel stale"]
POS["Positioning - flip / shift near edges"] --> LIST- State:
isOpen,highlightedIndex,selected,query,loading. Handle transitions as a small state machine (closed → open → selecting → closed). - Portal + positioning: render the list in a portal attached to
bodyso it isn't clipped byoverflow: hiddenparents. Position it next to the trigger, and flip above when there's no room below (a library like Floating UI). - Async options: debounce input (~200 ms), cancel older requests (AbortController) so results don't arrive out of order, and show loading and "no results" states.
4) Accessibility (important in interviews)
- Use the ARIA combobox pattern: the trigger has
role="combobox",aria-expanded,aria-controls,aria-activedescendant(the highlighted option). The list hasrole="listbox", options haverole="option"witharia-selectedandaria-disabled. - Focus management: focus stays on the input/trigger while arrows move the "active descendant". On close, focus returns to the trigger.
- Keyboard map: ↓/↑ move, Home/End jump, Enter/Space select, Esc close, typing letters jumps to matching options (type-ahead).
- Visible focus styles, sufficient contrast, and screen-reader announcements for counts ("12 results").
5) Performance
- Virtualization: only render the ~15 visible rows (plus a buffer) for large lists, using fixed row heights to make scroll math cheap.
- Memoize filtered results, and avoid re-rendering all options on each highlight change (pass the index, and let rows re-render only when their own state changes).
- Lazy-load heavy option content (avatars).
6) Quality
- Tests: unit tests for the hook's state machine, interaction tests (keyboard), and automated accessibility checks (axe).
- Theming through design tokens (CSS variables), with no hard-coded colors.
- Semantic versioning for the component, a changelog, and deprecation warnings for renamed props.
7) Wrap-Up
Expose a simple <Select> API that supports both controlled and uncontrolled use, single or multi-select, search, async loading and custom rendering, built on a headless useSelect hook with a clear state machine. Render the list in a positioned portal, follow the ARIA combobox/listbox pattern with proper focus and keyboard behavior, virtualize long lists, debounce and cancel async searches, and ship it with tests, theming tokens and versioning.