CASE STUDY

Reusable Dropdown / Select Component (Frontend System Design)

3 min read·540 words·Intermediate

Asked at

1 candidate report in Jun 2026

How to use this case study

SDE-2 / Mid

Design the component's props, controlled vs uncontrolled state, and keyboard and mouse behavior.

SDE-3 / Senior

Go deeper on accessibility (ARIA combobox/listbox), focus management, positioning in a portal, search/filter, multi-select and async options.

Staff / Principal

Discuss performance with 10K+ options (virtualization), theming and composition APIs, testing, and versioning a design-system component.


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 value is passed, the parent owns the state (controlled). Otherwise the component keeps its own (uncontrolled, using defaultValue). 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 body so it isn't clipped by overflow: hidden parents. 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 has role="listbox", options have role="option" with aria-selected and aria-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.

More Case Studies

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with our AI interviewer.

Start System Design Interview →