•CASE STUDY

Reusable Dropdown / Select Component (Frontend System Design)

5 min read·827 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
  • 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
  • Versioning a design-system component

Problem RestatementProblem

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.

RequirementsRequirements

  • 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.

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.

Internal Structure

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
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.

Deep Dive — Making the dropdown usable without a mouseDeep dive

A design system component is used by dozens of teams and inherits whatever accessibility it has. Getting this wrong once propagates everywhere.

Weak

A styled div with a click handler

A <div> for the trigger, a <div> list that appears on click, options as <div>s.

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  D["div trigger + div list"] --> TAB["Not in the tab order - keyboard users cannot reach it"]
  D --> SR["Screen reader announces nothing - no role, no state"]
  D --> ARROW["Arrow keys scroll the page instead of moving selection"]
  D --> ESC["Escape does nothing - no way out without a mouse"]

Visually it is a dropdown; to the accessibility tree it is a box of text. Anyone navigating by keyboard or screen reader cannot operate it at all, which in many organisations makes it unshippable regardless of how it looks.

Good

Use a native <select>

The browser provides focus, keyboard handling, type-ahead and screen-reader semantics for free, and the mobile picker is the platform's own.

For a plain list of options this is genuinely the right answer, and worth saying so. Its limit is styling and content: <option> cannot contain markup, so no icons, no two-line options, no avatars, no async search — which is exactly what a design system dropdown is asked for. Once you need those, <select> is no longer an option, and everything it provided has to be rebuilt deliberately.

Best

Implement the ARIA combobox pattern properly

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  T["Trigger: role=combobox, aria-expanded, aria-controls"] --> L["List: role=listbox"]
  L --> O["Options: role=option, aria-selected, aria-disabled"]
  T --> AD["aria-activedescendant points at the highlighted option"]
  AD --> FOCUS["DOM focus stays on the trigger - only the pointer moves"]
  K["Arrow keys / Home / End"] --> AD
  K2["Enter or Space selects, Escape closes"] --> RET["Focus returns to the trigger"]
  K3["Typing letters - type-ahead"] --> AD
  L --> ANN["Announce result counts - '12 results'"]
  • aria-activedescendant, not moving focus. DOM focus stays on the trigger while the highlighted option changes. Moving real focus into the list breaks typing in a combobox and makes the return path fragile.
  • The full keyboard map, because users expect the native one: arrows to move, Home and End to jump, Enter or Space to select, Escape to close, and letter keys for type-ahead.
  • Return focus to the trigger on close. Leaving focus on a removed element sends the user back to the top of the document — one of the most common bugs in hand-built dropdowns.
  • Announce state changes, including the result count when an async list loads, or a screen reader user has no idea the list changed.

Test it the way it will be used: tab to it, operate it entirely by keyboard with the screen reader on, and check that every state the eye can see has a corresponding announced state. Automated checks catch missing roles; they do not catch a focus trap.

PerformanceScale

  • 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).

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.

Wrap-UpWrap-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

Frequently Asked Questions

What is the Reusable Dropdown / Select Component (Frontend System Design) system design question?

Reusable Dropdown / Select Component (Frontend System Design) is a system design interview question asked at FAANG companies. It covers frontend, api design and tests your ability to design scalable, production-ready systems. InterviewSkool's breakdown walks you through requirements, API design, architecture, and trade-offs.

Which companies ask the Reusable Dropdown / Select Component (Frontend System Design) question?

Roblox have reportedly asked variations of this question in system design interviews. The exact wording may differ, but the core design challenges remain the same.

How should I prepare for the Reusable Dropdown / Select Component (Frontend System Design) interview question?

Start with the problem statement and scale estimates, then design the high-level architecture. Focus on the core components, data model, and API design. InterviewSkool's breakdown covers the full solution with mermaid diagrams and trade-off analysis to help you prep efficiently.

What level is the Reusable Dropdown / Select Component (Frontend System Design) question?

This question is suitable for SDE-2, SDE-3, and Staff engineer interviews. The level guidance on this page provides specific tips for each level — SDE-2 candidates should focus on core architecture, while Staff engineers should discuss trade-offs, monitoring, and incremental rollouts.

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with InterviewSkool's AI interviewer.

Start System Design Interview →