@cocorof/react-selector
v0.1.3
Published
Dependency-free React select — one component for both a fixed list and a searchable one, with async loading, multi-select, groups, keyboard and a11y, themable with CSS variables
Maintainers
Readme
@cocorof/react-selector
One React select for both cases, with no runtime dependencies.
A status filter with five fixed options does not need a search box — a text input above five items is noise. A list of every user in an org is unusable without one. Most select libraries make you choose a different component for each; this one decides from the list it was given, and you can override it.
npm i @cocorof/react-selectorimport { Selector } from "@cocorof/react-selector";
import "@cocorof/react-selector/styles.css";
// short, fixed → no search box
<Selector value={status} onChange={setStatus} options={[
{ value: "new", label: "New", count: 12 },
{ value: "read", label: "Read" },
]} />
// long → the search box appears on its own
<Selector value={userId} onChange={setUserId} options={users} />
// remote → you answer the query; the component stops filtering locally
<Selector value={userId} onChange={setUserId} options={results}
onSearch={setQuery} loading={isFetching} />Why it behaves the way it does
- The search box appears at 8 options (
searchThreshold), or wheneveronSearchis given. Force it either way withsearchable. onSearchturns off local filtering. Whoever answers the query owns the result; filtering it again client-side would hide rows the server deliberately returned.- Multi-select stays open. A menu that closes after each pick is a fight.
- Initials match, but narrowly.
cmofinds "Chief Marketing Officer";user 12does not match "User 21", because a subsequence rule applied to long queries lies. - The menu is
position: fixed. An absolutely positioned menu inherits everyoverflow: hiddenbetween it and the page, which is how dropdowns end up clipped inside cards. It flips above the trigger when the viewport would cut it off.
Props
| prop | type | note |
|---|---|---|
| value | string \| string[] \| null | array with multiple |
| onChange | (value) => void | string, or string[] with multiple |
| options | SelectorOption[] \| SelectorGroup[] | groups render with headers |
| searchable | boolean | override the automatic decision |
| searchThreshold | number | default 8 |
| onSearch | (q: string) => void | remote mode |
| loading | boolean | shows a loading row |
| multiple clearable disabled | boolean | |
| size | "sm" \| "md" \| "lg" | 32 / 36 / 40px |
| labels | SelectorLabels | every string is replaceable |
| renderValue | (selected) => ReactNode | custom trigger content |
SelectorOption: { value, label, keywords?, icon?, count?, description?, disabled? }.
keywords is what search matches when label is not a string.
Theming
Every colour is a CSS variable on .rsel; there is no styling to override.
.rsel { --rsel-accent: #7c3aed; --rsel-radius: 8px; }Light and dark follow prefers-color-scheme. Pin one with data-rsel-theme="light" | "dark".
Keyboard and a11y
combobox trigger, listbox menu, option rows with aria-selected. Arrows move,
Home/End jump, Enter picks, Escape closes and returns focus, Tab closes.
MIT
