Build a Multi-Select with Tags

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:

  1. Input field for typing
  2. Tag list (selected items)
  3. 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-controls pointing 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.

Scroll to Top