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 -
AsyncSelectwithloadOptionscallback - Creatable -
CreatableSelectlets users add new options - Custom components - replace any part of the UI (Option, Menu, Control)
- Groups -
{ label: 'Group', options: [...] }format - Portal -
menuPortalTargetto 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
unstyledprop +classNamesorstylesprop for full style control without fighting specificity menuPortalTarget- if the select is inside a table cell oroverflow: hiddencontainer, the dropdown will be clipped. SetmenuPortalTarget={document.body}andmenuPosition="fixed"to fix itvaluemust match option references - for controlled mode, thevalueprop must be the actual option object (ornull), not just the value string
Related
- 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