mrkeyoor.com_
Fri 07 Aug 19:04 UTC
npmWeb Frontendupdated 07 Aug 2026

react-select

react-select replaces the native HTML select with a React combobox that does the things a native select cannot: type-to-filter, multi-value tags, option groups, async option loading, and letting the user create a value that was not in the list. You give it an options array of { value, label } objects and an onChange, and you get a controlled component whose value is the selected option object rather than a raw string. Every visual piece is a swappable subcomponent (Control, Menu, Option, MultiValue, DropdownIndicator and about fifteen more), so you can restyle with the styles prop, restyle with class names, or replace a piece outright with your own React component. It is written in TypeScript since v5 and generic over your option type, your group type, and whether it is multi-select, which is what makes onChange narrow correctly. Styling runs through emotion at runtime, which is the single fact that decides whether this library fits your app.

Verdict

Still the fastest way to get a multi-select with async loading and tags into a React form, and the component injection API means you rarely hit a wall you cannot climb. Weigh it against a maintenance record of one commit in 2026 and 393 open issues, and against 29.1 kB of emotion-backed runtime that a Tailwind app has no other reason to carry.

API stability5/5v5 landed in 2021 and the prop surface has only been added to since: 5.8.0 brought accessibility attributes, 5.9.0 added React 19 to the peer range, 5.10.0 exported one more type. Code written against 5.2 still compiles against 5.10.2, which is partly a compliment and partly a symptom of how little is changing.
Docs4/5react-select.com is a proper site with live editable examples for async, creatable, portals, styling and custom components, plus a TypeScript guide and upgrade guides back to v2. It loses a point because three overlapping styling systems are documented side by side with no recommendation, and the performance ceiling on large option lists is never mentioned.
Maintenance2/5The last npm publish was 5.10.2 on 2025-07-11 and master has one commit dated 2026-07-16 in the whole of 2026. 393 issues are open, 489 counting PRs, many with reproductions and no maintainer reply. Nothing is broken today and it is not archived, but treat it as feature-frozen software you will be maintaining yourself if you hit an edge case.
Ecosystem5/5Roughly 9M weekly downloads and the default answer in most React form tutorials. Enough of the ecosystem is built on it that wrappers exist for the gaps: react-windowed-select for virtualization, react-select-async-paginate for cursor pagination, and first-class recipes in react-hook-form and Formik docs.

Use it if

  • You need behaviour that a native select cannot do and you do not want to build it: searchable filtering, multi-select with removable tags, grouped options with headings, and async loading tied to what the user types
  • You need users to add values that are not in the list, which CreatableSelect from react-select/creatable handles including the onCreateOption callback and the create-new row in the menu
  • You want to replace pieces of the UI rather than fight the defaults: the components prop takes your own Option, MultiValue or DropdownIndicator and hands it the same props the built-in one gets, so a custom row with an avatar is about twenty lines
  • You are on TypeScript and want the value type to follow the config: the Option, IsMulti and Group generics make onChange receive SingleValue<Option> or MultiValue<Option> correctly instead of any
  • Your app already ships emotion, in which case the styling engine is a sunk cost and you are only adding the component
Skip it if

Setup reality

npm install react-select pulls nine dependencies including two emotion packages, so expect the install to be bigger than the component suggests. React 16.8 through 19 are accepted as peers, and if you are on React 19 you want 5.9.0 or newer since that is where 19 entered the peer range and 5.8.3 fixed the type incompatibilities. The first real friction is that value is an option object, not a value: passing value={'chocolate'} silently renders an empty control, and you have to do options.find(o => o.value === id) yourself on every render or keep the whole option in state. The second is server rendering. react-select generates ids for its inner elements, and until 5.10.2 an isAppleDevice check ran during render and produced hydration mismatches; even on current versions the fix for id warnings is to pass a stable instanceId. The third is menus getting clipped by an overflow: hidden ancestor, which is solved with menuPortalTarget={document.body} plus a menuPortal style raising z-index, and that in turn requires guarding document access in SSR. Styling has three separate systems (the styles prop taking base plus state, the theme prop, and classNamePrefix or classNames), and the docs do not steer you to one.

Patterns

Control the value from React statecontrolled-single-select

import { useState } from 'react';
import Select from 'react-select';

const options = [
  { value: 'chocolate', label: 'Chocolate' },
  { value: 'vanilla', label: 'Vanilla' },
];

export function FlavorPicker() {
  const [flavor, setFlavor] = useState(null);
  return <Select options={options} value={flavor} onChange={setFlavor} isClearable />;
}

value must be the option object, not the string inside it. Storing only the id in state and passing value={id} renders an empty control with no warning, which is the single most common first-hour bug with this library.

Type the value so onChange narrows correctlytypescript-generics

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

type Flavor = { value: string; label: string };

function handle(next: SingleValue<Flavor>, meta: ActionMeta<Flavor>) {
  if (meta.action === 'clear') return setFlavor(null);
  setFlavor(next);
}

<Select<Flavor> options={options} onChange={handle} />;

SingleValue<T> is T | null and MultiValue<T> is readonly T[], picked by the IsMulti generic. The second argument tells you why the change happened (select-option, remove-value, clear, create-option), which is the only way to distinguish a cleared field from a deselect.

Let the user pick several values as tagsmulti-select

import Select, { type MultiValue } from 'react-select';

const [tags, setTags] = useState<readonly Tag[]>([]);

<Select
  isMulti
  options={allTags}
  value={tags}
  onChange={(next: MultiValue<Tag>) => setTags(next)}
  closeMenuOnSelect={false}
  hideSelectedOptions={false}
/>

onChange hands you a readonly array, so pushing into it fails type checking and you must spread. closeMenuOnSelect={false} is not the default and users picking five tags in a row will notice its absence immediately.

Use your own objects without remapping themcustom-option-shape

type User = { id: number; firstName: string; lastName: string };

<Select<User>
  options={users}
  getOptionValue={(u) => String(u.id)}
  getOptionLabel={(u) => `${u.firstName} ${u.lastName}`}
  isOptionDisabled={(u) => u.suspended}
/>

This avoids building a parallel { value, label } array on every render, which also fixes the subtle bug where new object identities break the selected-value comparison. getOptionValue must return a string, so wrap numeric ids in String().

Render options under group headingsgrouped-options

import Select, { type GroupBase } from 'react-select';

const grouped: GroupBase<Flavor>[] = [
  { label: 'Classic', options: [chocolate, vanilla] },
  { label: 'Seasonal', options: [pumpkin] },
];

<Select options={grouped} formatGroupLabel={(g) => <b>{g.label} ({g.options.length})</b>} />

Groups and flat options cannot be mixed in one array; every entry must have an options key or none of them may. Filtering searches the child options and hides a group heading once all of its children are filtered out.

Load options from the server as the user typesasync-search

import AsyncSelect from 'react-select/async';

const loadOptions = async (input: string) => {
  if (input.length < 2) return [];
  const res = await fetch(`/api/users?q=${encodeURIComponent(input)}`);
  return res.json();
};

<AsyncSelect cacheOptions defaultOptions loadOptions={loadOptions} />

There is no built-in debounce, so every keystroke fires a request unless you wrap loadOptions yourself. cacheOptions keys results by input string and never expires until the prop value changes, and defaultOptions={true} eagerly calls loadOptions('') on mount.

Let the user add a value that is not in the listcreate-new-option

import CreatableSelect from 'react-select/creatable';

<CreatableSelect
  isMulti
  options={tags}
  onCreateOption={(input) => {
    const created = { value: slugify(input), label: input };
    setTags((prev) => [...prev, created]);
    setValue((prev) => [...prev, created]);
  }}
  formatCreateLabel={(input) => `Add "${input}"`}
/>

When you supply onCreateOption, react-select stops managing the new value for you: it calls your handler and nothing appears until you add it to both the options and the value yourself. Omit the prop and it handles both automatically.

Override the look through the styles propcustom-styles

import Select, { type StylesConfig } from 'react-select';

const styles: StylesConfig<Flavor, false> = {
  control: (base, state) => ({
    ...base,
    borderColor: state.isFocused ? '#2563eb' : '#d1d5db',
    boxShadow: 'none',
    minHeight: 38,
  }),
  option: (base, state) => ({ ...base, backgroundColor: state.isSelected ? '#2563eb' : base.backgroundColor }),
};

<Select styles={styles} options={options} />

Forgetting to spread base drops the layout rules the component needs and the control collapses. Define the styles object outside the component or memoize it, because a fresh object every render makes emotion recompute and reinsert the class names.

Drive the appearance with Tailwind classestailwind-unstyled

<Select
  unstyled
  options={options}
  classNames={{
    control: (s) => `border rounded-md px-2 ${s.isFocused ? 'border-blue-600' : 'border-gray-300'}`,
    menu: () => 'mt-1 border rounded-md bg-white shadow-lg',
    option: (s) => (s.isFocused ? 'px-3 py-2 bg-gray-100' : 'px-3 py-2'),
    multiValue: () => 'bg-gray-100 rounded px-1 mr-1',
  }}
/>

unstyled removes the default emotion styles but not emotion itself, so the runtime and its two packages stay in your bundle. Without unstyled the emotion rules usually win the cascade against Tailwind utilities and your classes appear to do nothing.

Stop the menu being clipped by a scrolling parentmenu-portal

<Select
  options={options}
  menuPortalTarget={typeof document !== 'undefined' ? document.body : null}
  menuPosition="fixed"
  styles={{ menuPortal: (base) => ({ ...base, zIndex: 9999 }) }}
/>

Any ancestor with overflow: hidden or a transform clips the menu, and inside a modal it usually disappears entirely. Portalling fixes the clipping but drops the menu out of the modal's z-index context, so the menuPortal z-index override is not optional.

Replace the option row with your own componentcustom-option-component

import Select, { components, type OptionProps } from 'react-select';

function UserOption(props: OptionProps<User>) {
  return (
    <components.Option {...props}>
      <img src={props.data.avatarUrl} width={20} height={20} alt="" />
      <span>{props.data.firstName}</span>
    </components.Option>
  );
}

<Select<User> options={users} components={{ Option: UserOption }} />

Wrapping components.Option keeps the click handling, keyboard focus and aria-selected wiring; rendering a bare div instead breaks selection with the keyboard. Declare the component outside render or React remounts the whole menu on every keystroke.

Wire it into react-hook-formreact-hook-form

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

<Controller
  name="country"
  control={control}
  rules={{ required: 'Pick a country' }}
  render={({ field }) => (
    <Select {...field} options={countries} isClearable />
  )}
/>

Controller is required because react-select does not forward a ref to a real input, so register() has nothing to attach to. field.value is the whole option object, so your submit handler receives { value, label } and needs to unwrap it before posting.

Alternatives

PackageRegistryPick it when
downshiftnpmYou want the combobox keyboard and accessibility logic as hooks and intend to render and style everything yourself
@radix-ui/react-selectnpmYou need an accessible unstyled single-select primitive that works with Tailwind and ships no CSS-in-JS runtime
cmdknpmThe real requirement is a searchable command palette or filterable list rather than a form field
react-windowed-selectnpmYou are staying on react-select but need thousands of options to scroll without dropping frames