Multi-select inputs (with selected items rendered as removable tags) appear everywhere — Gmail, Slack, Notion, every signup form with multi-tag categorization. The interview tests whether you understand the combobox pattern, keyboard interactions, and accessibility.
Functional requirements
- Type to filter options
- Select option → adds tag
- Tag has X to remove
- Backspace at empty cursor removes last tag
- Keyboard navigation (arrow keys, Enter)
- Optionally creatable (allow new tags not in list)
Architecture
Three pieces:
- Input field for typing
- Tag list (selected items)
- Dropdown of options matching input
State
- Selected tags (array)
- Input text
- Filtered options (derived)
- Highlighted option index
The ARIA combobox pattern
Modern WAI-ARIA pattern (revised in 1.2):
- Wrapper has
role="combobox" - Input has
aria-controlspointing to listbox - Input has
aria-expanded - Listbox has
role="listbox" - Each option:
role="option",aria-selected - Active descendant via
aria-activedescendant
Keyboard interactions
- Down: open dropdown / next option
- Up: previous option
- Enter: select highlighted option
- Escape: close dropdown
- Backspace at empty cursor: remove last tag
- Tab: typically closes the dropdown
Filtering
Client-side: options.filter(o => o.label.toLowerCase().includes(query.toLowerCase()))
For large option lists (1000+): server-side with debounced API call.
Async option loading
For tag types like “people in your org,” fetch on each keystroke:
- Debounce 200–300ms
- Cancel previous request
- Show loading indicator
- Cache recent results
Creatable tags
“Add new” tag option when user types text not in the list. Common pattern:
{filteredOptions.length === 0 && query && (
<Option onClick={() => createTag(query)}>
Create "{query}"
</Option>
)}
Tag rendering
- Pill shape with text + remove button
- Truncate long text with tooltip on hover
- Different colors for tag categories (optional)
Mobile
- Tap to focus input
- Tap option to select
- Swipe left on tag to delete (some apps)
- Long-press for context menu
Async creation
For tags that need server creation (custom labels):
- Optimistic add (show tag immediately)
- API call in background
- If creation fails, remove the tag and show error
Common mistakes
- No keyboard navigation
- Backspace does not remove last tag
- aria-activedescendant set incorrectly (focus jumps when typing)
- Filtering blocks main thread for large option lists
- No loading state for async options
Library options
- Downshift: headless combobox, full control
- react-select: popular, full-featured
- cmdk: command palette / combobox built for Linear-style UI
- Radix Combobox: in 2024+
Frequently Asked Questions
Should I use react-select or build from scratch?
react-select for production speed. Build from scratch for interview practice or unusual customization.
How do I handle tags with special characters?
Escape display; preserve raw value in state. Validate before allowing creation.
What if the user is offline?
Cache options locally. Allow creation, queue for sync.