react-select-max
v0.1.0
Published
A powerful, searchable React select — multi-select, async loading, creatable options, grouping, and built-in virtualization for large lists. Zero-config styling, no complex style objects to learn.
Maintainers
Readme
react-select-max
A powerful, searchable select for React — everything react-select
does, plus built-in virtualization for large lists, and none of the
styles={{ control: (base) => ... }} headaches.
- 🔍 Search built in — type to filter, no setup
- 🏷️ Multi-select with removable tags
- ⚡ Async loading — search a remote API with automatic debouncing
- ➕ Creatable — let users add options that aren't in the list
- 📂 Grouped options
- 🚀 Built-in virtualization — smoothly handles thousands of options with
no extra package (react-select needs a separate
react-windowsetup for this) - 🎨 Zero-config styling — looks good immediately; override with plain CSS variables instead of a nested JS style-function API
- ♿ Full keyboard navigation (arrows, Enter, Escape, Backspace-to-remove-tag)
- 📦 Tiny, ships ESM + CJS + TypeScript types
Install
npm install react-select-maxQuick start
import { useState } from "react";
import { SelectSearch } from "react-select-max";
const options = [
{ value: "apple", label: "Apple" },
{ value: "banana", label: "Banana" },
{ value: "cherry", label: "Cherry" },
];
function Example() {
const [value, setValue] = useState(null);
return (
<SelectSearch
options={options}
value={value}
onChange={setValue}
placeholder="Pick a fruit"
/>
);
}No CSS import, no style objects to configure — it just works.
Multi-select
const [value, setValue] = useState([]);
<SelectSearch options={options} isMulti value={value} onChange={setValue} />value and the argument to onChange are arrays of full option objects.
Users can remove a tag by clicking its ✕, or by pressing Backspace when the
search box is empty.
Async (load options from an API)
async function searchUsers(query) {
const res = await fetch(`/api/users?q=${query}`);
return res.json(); // [{ value, label }, ...]
}
<SelectSearch
loadOptions={searchUsers}
defaultOptions={[]} // shown before the user types anything
value={value}
onChange={setValue}
/>Requests are automatically debounced (~300ms) and stale responses are discarded if a newer request comes back first — no race conditions to manage yourself.
Creatable (let users add new options)
<SelectSearch
options={options}
isCreatable
value={value}
onChange={setValue}
/>If the typed text doesn't match an existing option, a "Create '...'" row
appears. By default, selecting it creates { value: text, label: text }
and selects it — no extra wiring needed. If you want to control creation
yourself (e.g. to save it to a database first), pass onCreateOption:
<SelectSearch
options={options}
isCreatable
onCreateOption={(text) => {
const newOption = { value: text, label: text };
setOptions((prev) => [...prev, newOption]);
setValue(newOption);
}}
/>Grouped options
<SelectSearch
options={[
{ label: "Fruits", options: [{ value: "apple", label: "Apple" }] },
{ label: "Vegetables", options: [{ value: "carrot", label: "Carrot" }] },
]}
/>Large lists (virtualization)
Once there are more than 100 options, react-select-max automatically
switches to rendering only the visible rows — no extra setup, and no need
for a separate windowing library.
<SelectSearch options={twoThousandOptions} />Adjust or disable the threshold if you want:
<SelectSearch options={list} virtualizeThreshold={50} />
<SelectSearch options={list} virtualizeThreshold={false} /> {/* always render everything */}Note: virtualization currently uses a uniform row height and applies to flat option lists; very large grouped lists render normally (without windowing) for now.
Styling
Override the look with CSS variables — no style-function API to learn:
:root {
--rsm-border: #cbd5e1;
--rsm-focus: #7c3aed;
--rsm-tag-bg: #f3e8ff;
--rsm-tag-text: #6b21a8;
--rsm-selected-bg: #f3e8ff;
}See src/injectStyles.ts for the full list of
variables and default values.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| options | Option[] \| OptionGroup[] | — | Required. Flat or grouped options |
| value | Option \| Option[] \| null | — | Controlled selected value |
| defaultValue | Option \| Option[] \| null | null | Uncontrolled initial value |
| onChange | (value) => void | — | Called on selection change |
| isMulti | boolean | false | Allow multiple selections |
| searchable | boolean | true | Show a search input |
| isClearable | boolean | true | Show a clear (✕) button |
| isDisabled | boolean | false | Disable the control |
| isCreatable | boolean | false | Allow creating new options |
| onCreateOption | (text: string) => void | — | Custom create handler |
| loadOptions | (query: string) => Promise<Option[]> | — | Enable async mode |
| defaultOptions | Option[] | [] | Options shown before typing, in async mode |
| placeholder | string | "Select..." | — |
| noOptionsMessage | string | "No options found" | — |
| isLoading | boolean | false | Force-show the loading spinner |
| virtualizeThreshold | number \| false | 100 | Row count that triggers virtualization |
| itemHeight | number | 36 | Row height in px, used for virtualization math |
| maxMenuHeight | number | 280 | Max open-menu height in px |
| className | string | — | Extra class on the outer container |
| name | string | — | For native <form> submission |
Try the live example
git clone https://github.com/StevekWP/react-select-max
cd react-select-max
npm install
npm run exampleThe demo covers single-select, multi-select, async, creatable, grouped, and a 2,000-item virtualized list.
License
MIT
