Skip to content
Forms

Select — Component Library Guide

A flexible and beautiful Select Input control for React with multiselect, async loading, creatable options, and complete keyboard accessibility.

TypeScript MIT
4.5M weekly downloads
27.9k GitHub stars
~28kB bundle size
v5.9.0 latest version
selectmultiselectasynccreatableaccessible

TL;DR: React Select handles what native <select> can't: multi-select with tags, async search, and user-creatable options. Use <Controller> from React Hook Form to connect it. Larger bundle at ~28kB but thorough customization. Official docs: react-select.com.

What is React Select?

React Select is the feature-complete select input for React. It handles the cases that a native <select> can't: multi-select with tags, async search (type to load options from an API), and user-creatable options. The downside is its ~28kB bundle size and opinionated default styles - but the customisation API is thorough.

When to use it

Use React Select for:

  • Multi-select tag inputs - e.g., tagging, category selection
  • Async search - type to search an API and select from results
  • Creatable selects - let users type new values if not in the list
  • Complex select state - grouped options, clearable, loading indicators

For a simple single-select dropdown without async, native <select> or a Radix/Headless UI combobox is lighter.

Picking the right variant for the job

React Select isn't one component, it's three that share a core. Knowing which to grab saves a lot of fiddling. The base Select is for static option lists you already have in memory: a country picker, a status field, a list of tags you control. If the list is short and fixed, this is all you need.

AsyncSelect is the one to reach for when options live on a server. You give it a loadOptions function, and as the user types, it calls your API and shows the results. This is the pattern for searching users, products, or anything with thousands of possible values you can't ship to the browser up front. Turn on cacheOptions so repeated searches don't refetch, and defaultOptions so the menu isn't empty before the user types anything. One detail people miss: debounce the input yourself if your API is rate-limited, because loadOptions fires on each keystroke by default.

CreatableSelect lets users add values that aren't in the list. Think tag inputs or a "label" field where you can't predict every option in advance. It pairs naturally with isMulti for free-form tagging. All three variants share the same props, so moving between them is mostly an import change.

Styling without fighting specificity

The honest downside of React Select is its default look. It's clean but opinionated, and it injects its own CSS, which historically led to specificity wars when you tried to override it. The modern answer is the unstyled prop. Switch it on and React Select strips its built-in styles, leaving you a clean slate.

From there you have two routes. The styles prop takes objects per internal slot, like control, menu, option, and multiValue, and is good for one-off inline tweaks. The classNames prop instead lets you attach your own class names to each slot, which is the better fit when you're on Tailwind or a utility-first setup. Combining unstyled with classNames gives you a select that looks like it belongs in your design system instead of looking like a React Select demo. It's more upfront work than a styled library, but you end up with exactly what you want.

The two bugs everyone hits

Two issues account for most of the support questions about this library, so it's worth knowing them before you write a line of code.

The first is the clipped dropdown. Put a React Select inside a table cell, a modal, or any container with overflow: hidden, and the menu gets cut off at the container's edge. The fix is to render the menu in a portal: set menuPortalTarget={document.body} and menuPosition="fixed". This lifts the dropdown out of the clipping parent and onto the page body, where it can extend freely. If your dropdown is mysteriously chopped in half, this is almost always why.

The second is the value identity trap. In controlled mode, the value prop must be the actual option object, not just the string you stored. If you keep "react" in state and pass it directly, the select shows nothing selected, because it compares object references, not strings. The fix is to look up the matching option object from your list and pass that. For isMulti, filter your options array down to the selected values. This is also why the React Hook Form integration maps values back to objects in the value prop while storing plain values in form state.

Accessibility you get for free

React Select ships keyboard and screen reader support out of the box, which is the strongest argument for using it over a hand-rolled dropdown. Arrow keys move through options, Enter selects, Escape closes, and typing filters. It announces the active option and selection state to assistive tech through ARIA attributes you don't have to wire up yourself. For multi-select, each chosen value gets its own removable element that's reachable by keyboard. Replicating this correctly from scratch is a real project on its own, and getting it subtly wrong locks out keyboard and screen reader users, so the bundle size buys you genuine accessibility coverage.

Key Features

  • Multi-select - tag-style multi-value with clear per-item
  • Async - AsyncSelect with loadOptions callback
  • Creatable - CreatableSelect lets users add new options
  • Custom components - replace any part of the UI (Option, Menu, Control)
  • Groups - { label: 'Group', options: [...] } format
  • Portal - menuPortalTarget to render menu in document body

Installation

npm install react-select

Basic Usage

import Select from 'react-select'

const options = [
  { value: 'react', label: 'React' },
  { value: 'vue', label: 'Vue' },
  { value: 'svelte', label: 'Svelte' },
]

function FrameworkPicker() {
  return (
    <Select
      options={options}
      placeholder="Select a framework..."
      isClearable
      isSearchable
    />
  )
}

Async Select

import AsyncSelect from 'react-select/async'

async function loadUsers(inputValue: string) {
  const res = await fetch(`/api/users?search=${inputValue}`)
  const users = await res.json() as User[]
  return users.map(u => ({ value: u.id, label: u.name }))
}

function UserSearch({ onChange }: { onChange: (user: Option) => void }) {
  return (
    <AsyncSelect
      loadOptions={loadUsers}
      onChange={(selected) => selected && onChange(selected)}
      placeholder="Search users..."
      cacheOptions
      defaultOptions
    />
  )
}

Integration with React Hook Form

import { Controller } from 'react-hook-form'
import Select from 'react-select'

<Controller
  name="tags"
  control={control}
  render={({ field }) => (
    <Select
      {...field}
      options={tagOptions}
      isMulti
      onChange={(selected) => field.onChange(selected?.map(s => s.value))}
      value={tagOptions.filter(opt => field.value?.includes(opt.value))}
    />
  )}
/>

TypeScript Tips

Type the option shape with the generic parameter:

interface UserOption {
  value: string
  label: string
  role: 'admin' | 'user'
}

import Select, { type SingleValue } from 'react-select'

const [selected, setSelected] = useState<SingleValue<UserOption>>(null)

<Select<UserOption>
  options={userOptions}
  onChange={setSelected}
  getOptionLabel={(opt) => `${opt.label} (${opt.role})`}
/>

Common Gotchas

  • Style isolation - React Select injects global CSS for its default styles. Use unstyled prop + classNames or styles prop for full style control without fighting specificity
  • menuPortalTarget - if the select is inside a table cell or overflow: hidden container, the dropdown will be clipped. Set menuPortalTarget={document.body} and menuPosition="fixed" to fix it
  • value must match option references - for controlled mode, the value prop must be the actual option object (or null), not just the value string
  • React Hook Form - use <Controller> to integrate React Select into React Hook Form for validation and state management
  • React Dropzone - another controlled input component that pairs with React Hook Form for complex forms
  • cmdk - if you need a command palette with search rather than a dropdown, cmdk is the alternative

Frequently Asked Questions

How do I use React Select with React Hook Form?

Wrap Select inside a Controller component. In the render prop, spread field onto Select but override onChange to pass the value (or array of values for isMulti) back to the form. For isMulti, use field.onChange(selected?.map(s => s.value)) and reconstruct the option objects for the value prop.

How do I style React Select to match my design system?

Add the unstyled prop to remove all default styles, then use the classNames prop to apply your own CSS classes to each internal component (control, menu, option, multiValue, etc.). Alternatively, use the styles prop for inline style objects per component. The unstyled + classNames approach pairs well with Tailwind.

Why is the React Select dropdown being clipped inside my table or modal?

The dropdown is being clipped by an overflow: hidden parent. Fix it by adding menuPortalTarget={document.body} and menuPosition='fixed' to the Select component. This renders the dropdown menu in a portal attached to the document body, outside the clipping parent.

What is the difference between Select, AsyncSelect, and CreatableSelect?

Select is the base component for static option lists. AsyncSelect uses a loadOptions callback to fetch options asynchronously as the user types - ideal for server-side search. CreatableSelect allows users to type new values and create options not in the original list. All three share the same core API.

How do I set a default value in React Select?

Use the defaultValue prop for uncontrolled mode, passing the full option object (not just the value string). For controlled mode, use the value prop. For isMulti, pass an array of option objects. The value must always be the option object, not just the value property.